Disallow ERC20._transfer from the zero address. (#1752)

* Add requirement of non-zero from to ERC20 transfer.

* Add test for transferFrom zero address to behavior.

* Create ERC20.transfer behavior.

* Add tests for _transfer.

* Add changelog entry.

* Fix linter error.

* Delete repeated test.

* Fix hardcoded error prefix.

* Update CHANGELOG.md

Co-Authored-By: Francisco Giordano <frangio.1@gmail.com>

* Address review comments.

(cherry picked from commit ad18098d65)
This commit is contained in:
Nicolás Venturo
2019-05-16 11:50:54 -03:00
parent 74ef942bd1
commit f7ff3e7e67
5 changed files with 174 additions and 110 deletions

View File

@ -23,151 +23,127 @@ function shouldBehaveLikeERC20 (errorPrefix, initialSupply, initialHolder, recip
});
describe('transfer', function () {
describe('when the recipient is not the zero address', function () {
const to = recipient;
describe('when the sender does not have enough balance', function () {
const amount = initialSupply.addn(1);
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transfer(to, amount, { from: initialHolder }),
'SafeMath: subtraction overflow'
);
});
});
describe('when the sender has enough balance', function () {
const amount = initialSupply;
it('transfers the requested amount', async function () {
await this.token.transfer(to, amount, { from: initialHolder });
(await this.token.balanceOf(initialHolder)).should.be.bignumber.equal('0');
(await this.token.balanceOf(to)).should.be.bignumber.equal(amount);
});
it('emits a transfer event', async function () {
const { logs } = await this.token.transfer(to, amount, { from: initialHolder });
expectEvent.inLogs(logs, 'Transfer', {
from: initialHolder,
to: to,
value: amount,
});
});
});
});
describe('when the recipient is the zero address', function () {
const to = ZERO_ADDRESS;
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transfer(to, initialSupply, { from: initialHolder }),
`${errorPrefix}: transfer to the zero address`
);
});
});
shouldBehaveLikeERC20Transfer(errorPrefix, initialHolder, recipient, initialSupply,
function (from, to, value) {
return this.token.transfer(to, value, { from });
}
);
});
describe('transfer from', function () {
const spender = recipient;
describe('when the recipient is not the zero address', function () {
const to = anotherAccount;
describe('when the token owner is not the zero address', function () {
const tokenOwner = initialHolder;
describe('when the spender has enough approved balance', function () {
beforeEach(async function () {
await this.token.approve(spender, initialSupply, { from: initialHolder });
});
describe('when the recipient is not the zero address', function () {
const to = anotherAccount;
describe('when the initial holder has enough balance', function () {
const amount = initialSupply;
it('transfers the requested amount', async function () {
await this.token.transferFrom(initialHolder, to, amount, { from: spender });
(await this.token.balanceOf(initialHolder)).should.be.bignumber.equal('0');
(await this.token.balanceOf(to)).should.be.bignumber.equal(amount);
describe('when the spender has enough approved balance', function () {
beforeEach(async function () {
await this.token.approve(spender, initialSupply, { from: initialHolder });
});
it('decreases the spender allowance', async function () {
await this.token.transferFrom(initialHolder, to, amount, { from: spender });
describe('when the token owner has enough balance', function () {
const amount = initialSupply;
(await this.token.allowance(initialHolder, spender)).should.be.bignumber.equal('0');
});
it('transfers the requested amount', async function () {
await this.token.transferFrom(tokenOwner, to, amount, { from: spender });
it('emits a transfer event', async function () {
const { logs } = await this.token.transferFrom(initialHolder, to, amount, { from: spender });
(await this.token.balanceOf(tokenOwner)).should.be.bignumber.equal('0');
expectEvent.inLogs(logs, 'Transfer', {
from: initialHolder,
to: to,
value: amount,
(await this.token.balanceOf(to)).should.be.bignumber.equal(amount);
});
it('decreases the spender allowance', async function () {
await this.token.transferFrom(tokenOwner, to, amount, { from: spender });
(await this.token.allowance(tokenOwner, spender)).should.be.bignumber.equal('0');
});
it('emits a transfer event', async function () {
const { logs } = await this.token.transferFrom(tokenOwner, to, amount, { from: spender });
expectEvent.inLogs(logs, 'Transfer', {
from: tokenOwner,
to: to,
value: amount,
});
});
it('emits an approval event', async function () {
const { logs } = await this.token.transferFrom(tokenOwner, to, amount, { from: spender });
expectEvent.inLogs(logs, 'Approval', {
owner: tokenOwner,
spender: spender,
value: await this.token.allowance(tokenOwner, spender),
});
});
});
it('emits an approval event', async function () {
const { logs } = await this.token.transferFrom(initialHolder, to, amount, { from: spender });
describe('when the token owner does not have enough balance', function () {
const amount = initialSupply.addn(1);
expectEvent.inLogs(logs, 'Approval', {
owner: initialHolder,
spender: spender,
value: await this.token.allowance(initialHolder, spender),
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transferFrom(
tokenOwner, to, amount, { from: spender }), 'SafeMath: subtraction overflow'
);
});
});
});
describe('when the initial holder does not have enough balance', function () {
const amount = initialSupply.addn(1);
describe('when the spender does not have enough approved balance', function () {
beforeEach(async function () {
await this.token.approve(spender, initialSupply.subn(1), { from: tokenOwner });
});
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transferFrom(
initialHolder, to, amount, { from: spender }), 'SafeMath: subtraction overflow'
);
describe('when the token owner has enough balance', function () {
const amount = initialSupply;
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transferFrom(
tokenOwner, to, amount, { from: spender }), 'SafeMath: subtraction overflow'
);
});
});
describe('when the token owner does not have enough balance', function () {
const amount = initialSupply.addn(1);
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transferFrom(
tokenOwner, to, amount, { from: spender }), 'SafeMath: subtraction overflow'
);
});
});
});
});
describe('when the spender does not have enough approved balance', function () {
describe('when the recipient is the zero address', function () {
const amount = initialSupply;
const to = ZERO_ADDRESS;
beforeEach(async function () {
await this.token.approve(spender, initialSupply.subn(1), { from: initialHolder });
await this.token.approve(spender, amount, { from: tokenOwner });
});
describe('when the initial holder has enough balance', function () {
const amount = initialSupply;
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transferFrom(
initialHolder, to, amount, { from: spender }), 'SafeMath: subtraction overflow'
);
});
});
describe('when the initial holder does not have enough balance', function () {
const amount = initialSupply.addn(1);
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transferFrom(
initialHolder, to, amount, { from: spender }), 'SafeMath: subtraction overflow'
);
});
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transferFrom(
tokenOwner, to, amount, { from: spender }), `${errorPrefix}: transfer to the zero address`
);
});
});
});
describe('when the recipient is the zero address', function () {
const amount = initialSupply;
const to = ZERO_ADDRESS;
beforeEach(async function () {
await this.token.approve(spender, amount, { from: initialHolder });
});
describe('when the token owner is the zero address', function () {
const amount = 0;
const tokenOwner = ZERO_ADDRESS;
const to = recipient;
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transferFrom(
initialHolder, to, amount, { from: spender }), `${errorPrefix}: transfer to the zero address`
tokenOwner, to, amount, { from: spender }), `${errorPrefix}: transfer from the zero address`
);
});
});
@ -182,6 +158,72 @@ function shouldBehaveLikeERC20 (errorPrefix, initialSupply, initialHolder, recip
});
}
function shouldBehaveLikeERC20Transfer (errorPrefix, from, to, balance, transfer) {
describe('when the recipient is not the zero address', function () {
describe('when the sender does not have enough balance', function () {
const amount = balance.addn(1);
it('reverts', async function () {
await shouldFail.reverting.withMessage(transfer.call(this, from, to, amount),
'SafeMath: subtraction overflow'
);
});
});
describe('when the sender transfers all balance', function () {
const amount = balance;
it('transfers the requested amount', async function () {
await transfer.call(this, from, to, amount);
(await this.token.balanceOf(from)).should.be.bignumber.equal('0');
(await this.token.balanceOf(to)).should.be.bignumber.equal(amount);
});
it('emits a transfer event', async function () {
const { logs } = await transfer.call(this, from, to, amount);
expectEvent.inLogs(logs, 'Transfer', {
from,
to,
value: amount,
});
});
});
describe('when the sender transfers zero tokens', function () {
const amount = new BN('0');
it('transfers the requested amount', async function () {
await transfer.call(this, from, to, amount);
(await this.token.balanceOf(from)).should.be.bignumber.equal(balance);
(await this.token.balanceOf(to)).should.be.bignumber.equal('0');
});
it('emits a transfer event', async function () {
const { logs } = await transfer.call(this, from, to, amount);
expectEvent.inLogs(logs, 'Transfer', {
from,
to,
value: amount,
});
});
});
});
describe('when the recipient is the zero address', function () {
it('reverts', async function () {
await shouldFail.reverting.withMessage(transfer.call(this, from, ZERO_ADDRESS, balance),
`${errorPrefix}: transfer to the zero address`
);
});
});
}
function shouldBehaveLikeERC20Approve (errorPrefix, owner, spender, supply, approve) {
describe('when the spender is not the zero address', function () {
describe('when the sender has enough balance', function () {
@ -264,5 +306,6 @@ function shouldBehaveLikeERC20Approve (errorPrefix, owner, spender, supply, appr
module.exports = {
shouldBehaveLikeERC20,
shouldBehaveLikeERC20Transfer,
shouldBehaveLikeERC20Approve,
};

View File

@ -3,6 +3,7 @@ const { ZERO_ADDRESS } = constants;
const {
shouldBehaveLikeERC20,
shouldBehaveLikeERC20Transfer,
shouldBehaveLikeERC20Approve,
} = require('./ERC20.behavior');
@ -330,6 +331,20 @@ contract('ERC20', function ([_, initialHolder, recipient, anotherAccount]) {
});
});
describe('_transfer', function () {
shouldBehaveLikeERC20Transfer('ERC20', initialHolder, recipient, initialSupply, function (from, to, amount) {
return this.token.transferInternal(from, to, amount);
});
describe('when the sender is the zero address', function () {
it('reverts', async function () {
await shouldFail.reverting.withMessage(this.token.transferInternal(ZERO_ADDRESS, recipient, initialSupply),
'ERC20: transfer from the zero address'
);
});
});
});
describe('_approve', function () {
shouldBehaveLikeERC20Approve('ERC20', initialHolder, recipient, initialSupply, function (owner, spender, amount) {
return this.token.approveInternal(owner, spender, amount);