Skip to content

Commit 4dcdd29

Browse files
authored
StandardToken encapsulation (#1197)
* make StandardToken state variables private * simplify mocks * document new internal functions * fix link to ERC20 document * revert order of Transfer and Mint events * Revert "simplify mocks" This reverts commit 371fe3e. * add tests for new internal functions * add check for null account * add checks for balances and allowance * add inline docs to BurnableToken._burn * remove redundant checks and clarify why
1 parent 8d11dcc commit 4dcdd29

12 files changed

+230
-43
lines changed

contracts/examples/SimpleToken.sol

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ contract SimpleToken is StandardToken {
2222
* @dev Constructor that gives msg.sender all of existing tokens.
2323
*/
2424
constructor() public {
25-
totalSupply_ = INITIAL_SUPPLY;
26-
balances[msg.sender] = INITIAL_SUPPLY;
27-
emit Transfer(address(0), msg.sender, INITIAL_SUPPLY);
25+
_mint(msg.sender, INITIAL_SUPPLY);
2826
}
2927

3028
}

contracts/mocks/BurnableTokenMock.sol

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import "../token/ERC20/BurnableToken.sol";
66
contract BurnableTokenMock is BurnableToken {
77

88
constructor(address _initialAccount, uint256 _initialBalance) public {
9-
balances[_initialAccount] = _initialBalance;
10-
totalSupply_ = _initialBalance;
9+
_mint(_initialAccount, _initialBalance);
1110
}
1211

1312
}

contracts/mocks/ERC223TokenMock.sol

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ contract ERC223ContractInterface {
1111
contract ERC223TokenMock is StandardToken {
1212

1313
constructor(address _initialAccount, uint256 _initialBalance) public {
14-
balances[_initialAccount] = _initialBalance;
15-
totalSupply_ = _initialBalance;
14+
_mint(_initialAccount, _initialBalance);
1615
}
1716

1817
// ERC223 compatible transfer function (except the name)

contracts/mocks/PausableTokenMock.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import "../token/ERC20/PausableToken.sol";
77
contract PausableTokenMock is PausableToken {
88

99
constructor(address _initialAccount, uint _initialBalance) public {
10-
balances[_initialAccount] = _initialBalance;
10+
_mint(_initialAccount, _initialBalance);
1111
}
1212

1313
}

contracts/mocks/StandardTokenMock.sol

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,19 @@ import "../token/ERC20/StandardToken.sol";
77
contract StandardTokenMock is StandardToken {
88

99
constructor(address _initialAccount, uint256 _initialBalance) public {
10-
balances[_initialAccount] = _initialBalance;
11-
totalSupply_ = _initialBalance;
10+
_mint(_initialAccount, _initialBalance);
11+
}
12+
13+
function mint(address _account, uint256 _amount) public {
14+
_mint(_account, _amount);
15+
}
16+
17+
function burn(address _account, uint256 _amount) public {
18+
_burn(_account, _amount);
19+
}
20+
21+
function burnFrom(address _account, uint256 _amount) public {
22+
_burnFrom(_account, _amount);
1223
}
1324

1425
}

contracts/token/ERC20/BurnableToken.sol

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,15 @@ contract BurnableToken is StandardToken {
2525
* @param _value uint256 The amount of token to be burned
2626
*/
2727
function burnFrom(address _from, uint256 _value) public {
28-
require(_value <= allowed[_from][msg.sender]);
29-
// Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
30-
// this function needs to emit an event with the updated approval.
31-
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
32-
_burn(_from, _value);
28+
_burnFrom(_from, _value);
3329
}
3430

31+
/**
32+
* @dev Overrides StandardToken._burn in order for burn and burnFrom to emit
33+
* an additional Burn event.
34+
*/
3535
function _burn(address _who, uint256 _value) internal {
36-
require(_value <= balances[_who]);
37-
// no need to require value <= totalSupply, since that would imply the
38-
// sender's balance is greater than the totalSupply, which *should* be an assertion failure
39-
40-
balances[_who] = balances[_who].sub(_value);
41-
totalSupply_ = totalSupply_.sub(_value);
36+
super._burn(_who, _value);
4237
emit Burn(_who, _value);
43-
emit Transfer(_who, address(0), _value);
4438
}
45-
}
39+
}

contracts/token/ERC20/CappedToken.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ contract CappedToken is MintableToken {
2929
public
3030
returns (bool)
3131
{
32-
require(totalSupply_.add(_amount) <= cap);
32+
require(totalSupply().add(_amount) <= cap);
3333

3434
return super.mint(_to, _amount);
3535
}

contracts/token/ERC20/MintableToken.sol

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,8 @@ contract MintableToken is StandardToken, Ownable {
4141
canMint
4242
returns (bool)
4343
{
44-
totalSupply_ = totalSupply_.add(_amount);
45-
balances[_to] = balances[_to].add(_amount);
44+
_mint(_to, _amount);
4645
emit Mint(_to, _amount);
47-
emit Transfer(address(0), _to, _amount);
4846
return true;
4947
}
5048

contracts/token/ERC20/StandardToken.sol

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ import "../../math/SafeMath.sol";
88
* @title Standard ERC20 token
99
*
1010
* @dev Implementation of the basic standard token.
11-
* https://github.com/ethereum/EIPs/issues/20
11+
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
1212
* Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
1313
*/
1414
contract StandardToken is ERC20 {
1515
using SafeMath for uint256;
1616

17-
mapping(address => uint256) balances;
17+
mapping (address => uint256) private balances;
1818

19-
mapping (address => mapping (address => uint256)) internal allowed;
19+
mapping (address => mapping (address => uint256)) private allowed;
2020

21-
uint256 totalSupply_;
21+
uint256 private totalSupply_;
2222

2323
/**
2424
* @dev Total number of tokens in existence
@@ -156,4 +156,48 @@ contract StandardToken is ERC20 {
156156
return true;
157157
}
158158

159+
/**
160+
* @dev Internal function that mints an amount of the token and assigns it to
161+
* an account. This encapsulates the modification of balances such that the
162+
* proper events are emitted.
163+
* @param _account The account that will receive the created tokens.
164+
* @param _amount The amount that will be created.
165+
*/
166+
function _mint(address _account, uint256 _amount) internal {
167+
require(_account != 0);
168+
totalSupply_ = totalSupply_.add(_amount);
169+
balances[_account] = balances[_account].add(_amount);
170+
emit Transfer(address(0), _account, _amount);
171+
}
172+
173+
/**
174+
* @dev Internal function that burns an amount of the token of a given
175+
* account.
176+
* @param _account The account whose tokens will be burnt.
177+
* @param _amount The amount that will be burnt.
178+
*/
179+
function _burn(address _account, uint256 _amount) internal {
180+
require(_account != 0);
181+
require(balances[_account] > _amount);
182+
183+
totalSupply_ = totalSupply_.sub(_amount);
184+
balances[_account] = balances[_account].sub(_amount);
185+
emit Transfer(_account, address(0), _amount);
186+
}
187+
188+
/**
189+
* @dev Internal function that burns an amount of the token of a given
190+
* account, deducting from the sender's allowance for said account. Uses the
191+
* internal _burn function.
192+
* @param _account The account whose tokens will be burnt.
193+
* @param _amount The amount that will be burnt.
194+
*/
195+
function _burnFrom(address _account, uint256 _amount) internal {
196+
require(allowed[_account][msg.sender] > _amount);
197+
198+
// Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
199+
// this function needs to emit an event with the updated approval.
200+
allowed[_account][msg.sender] = allowed[_account][msg.sender].sub(_amount);
201+
_burn(_account, _amount);
202+
}
159203
}

test/token/ERC20/CappedToken.behavior.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { expectThrow } = require('../../helpers/expectThrow');
2+
const expectEvent = require('../../helpers/expectEvent');
23

34
const BigNumber = web3.BigNumber;
45

@@ -15,8 +16,8 @@ function shouldBehaveLikeCappedToken (minter, [anyone], cap) {
1516
});
1617

1718
it('should mint when amount is less than cap', async function () {
18-
const result = await this.token.mint(anyone, cap.sub(1), { from });
19-
result.logs[0].event.should.equal('Mint');
19+
const { logs } = await this.token.mint(anyone, cap.sub(1), { from });
20+
expectEvent.inLogs(logs, 'Mint');
2021
});
2122

2223
it('should fail to mint if the ammount exceeds the cap', async function () {

test/token/ERC20/MintableToken.behavior.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { assertRevert } = require('../../helpers/assertRevert');
2+
const expectEvent = require('../../helpers/expectEvent');
23

34
const BigNumber = web3.BigNumber;
45

@@ -7,6 +8,8 @@ require('chai')
78
.should();
89

910
function shouldBehaveLikeMintableToken (owner, minter, [anyone]) {
11+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
12+
1013
describe('as a basic mintable token', function () {
1114
describe('after token creation', function () {
1215
it('sender should be token owner', async function () {
@@ -104,11 +107,16 @@ function shouldBehaveLikeMintableToken (owner, minter, [anyone]) {
104107
it('emits a mint and a transfer event', async function () {
105108
const { logs } = await this.token.mint(owner, amount, { from });
106109

107-
logs.length.should.eq(2);
108-
logs[0].event.should.eq('Mint');
109-
logs[0].args.to.should.eq(owner);
110-
logs[0].args.amount.should.be.bignumber.equal(amount);
111-
logs[1].event.should.eq('Transfer');
110+
const mintEvent = expectEvent.inLogs(logs, 'Mint', {
111+
to: owner,
112+
});
113+
mintEvent.args.amount.should.be.bignumber.equal(amount);
114+
115+
const transferEvent = expectEvent.inLogs(logs, 'Transfer', {
116+
from: ZERO_ADDRESS,
117+
to: owner,
118+
});
119+
transferEvent.args.value.should.be.bignumber.equal(amount);
112120
});
113121
});
114122

test/token/ERC20/StandardToken.test.js

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
const { assertRevert } = require('../../helpers/assertRevert');
2+
const expectEvent = require('../../helpers/expectEvent');
3+
24
const StandardToken = artifacts.require('StandardTokenMock');
35

46
const BigNumber = web3.BigNumber;
@@ -68,11 +70,12 @@ contract('StandardToken', function ([_, owner, recipient, anotherAccount]) {
6870
it('emits a transfer event', async function () {
6971
const { logs } = await this.token.transfer(to, amount, { from: owner });
7072

71-
logs.length.should.eq(1);
72-
logs[0].event.should.eq('Transfer');
73-
logs[0].args.from.should.eq(owner);
74-
logs[0].args.to.should.eq(to);
75-
logs[0].args.value.should.be.bignumber.equal(amount);
73+
const event = expectEvent.inLogs(logs, 'Transfer', {
74+
from: owner,
75+
to: to,
76+
});
77+
78+
event.args.value.should.be.bignumber.equal(amount);
7679
});
7780
});
7881
});
@@ -486,4 +489,136 @@ contract('StandardToken', function ([_, owner, recipient, anotherAccount]) {
486489
});
487490
});
488491
});
492+
493+
describe('_mint', function () {
494+
const initialSupply = new BigNumber(100);
495+
const amount = new BigNumber(50);
496+
497+
it('rejects a null account', async function () {
498+
await assertRevert(this.token.mint(ZERO_ADDRESS, amount));
499+
});
500+
501+
describe('for a non null account', function () {
502+
beforeEach('minting', async function () {
503+
const { logs } = await this.token.mint(recipient, amount);
504+
this.logs = logs;
505+
});
506+
507+
it('increments totalSupply', async function () {
508+
const expectedSupply = initialSupply.plus(amount);
509+
(await this.token.totalSupply()).should.be.bignumber.equal(expectedSupply);
510+
});
511+
512+
it('increments recipient balance', async function () {
513+
(await this.token.balanceOf(recipient)).should.be.bignumber.equal(amount);
514+
});
515+
516+
it('emits Transfer event', async function () {
517+
const event = expectEvent.inLogs(this.logs, 'Transfer', {
518+
from: ZERO_ADDRESS,
519+
to: recipient,
520+
});
521+
522+
event.args.value.should.be.bignumber.equal(amount);
523+
});
524+
});
525+
});
526+
527+
describe('_burn', function () {
528+
const initialSupply = new BigNumber(100);
529+
const amount = new BigNumber(50);
530+
531+
it('rejects a null account', async function () {
532+
await assertRevert(this.token.burn(ZERO_ADDRESS, amount));
533+
});
534+
535+
describe('for a non null account', function () {
536+
it('rejects burning more than balance', async function () {
537+
await assertRevert(this.token.burn(owner, initialSupply.plus(1)));
538+
});
539+
540+
describe('for less amount than balance', function () {
541+
beforeEach('burning', async function () {
542+
const { logs } = await this.token.burn(owner, amount);
543+
this.logs = logs;
544+
});
545+
546+
it('decrements totalSupply', async function () {
547+
const expectedSupply = initialSupply.minus(amount);
548+
(await this.token.totalSupply()).should.be.bignumber.equal(expectedSupply);
549+
});
550+
551+
it('decrements owner balance', async function () {
552+
const expectedBalance = initialSupply.minus(amount);
553+
(await this.token.balanceOf(owner)).should.be.bignumber.equal(expectedBalance);
554+
});
555+
556+
it('emits Transfer event', async function () {
557+
const event = expectEvent.inLogs(this.logs, 'Transfer', {
558+
from: owner,
559+
to: ZERO_ADDRESS,
560+
});
561+
562+
event.args.value.should.be.bignumber.equal(amount);
563+
});
564+
});
565+
});
566+
});
567+
568+
describe('_burnFrom', function () {
569+
const initialSupply = new BigNumber(100);
570+
const allowance = new BigNumber(70);
571+
const amount = new BigNumber(50);
572+
573+
const spender = anotherAccount;
574+
575+
beforeEach('approving', async function () {
576+
await this.token.approve(spender, allowance, { from: owner });
577+
});
578+
579+
it('rejects a null account', async function () {
580+
await assertRevert(this.token.burnFrom(ZERO_ADDRESS, amount));
581+
});
582+
583+
describe('for a non null account', function () {
584+
it('rejects burning more than allowance', async function () {
585+
await assertRevert(this.token.burnFrom(owner, allowance.plus(1)));
586+
});
587+
588+
it('rejects burning more than balance', async function () {
589+
await assertRevert(this.token.burnFrom(owner, initialSupply.plus(1)));
590+
});
591+
592+
describe('for less amount than allowance', function () {
593+
beforeEach('burning', async function () {
594+
const { logs } = await this.token.burnFrom(owner, amount, { from: spender });
595+
this.logs = logs;
596+
});
597+
598+
it('decrements totalSupply', async function () {
599+
const expectedSupply = initialSupply.minus(amount);
600+
(await this.token.totalSupply()).should.be.bignumber.equal(expectedSupply);
601+
});
602+
603+
it('decrements owner balance', async function () {
604+
const expectedBalance = initialSupply.minus(amount);
605+
(await this.token.balanceOf(owner)).should.be.bignumber.equal(expectedBalance);
606+
});
607+
608+
it('decrements spender allowance', async function () {
609+
const expectedAllowance = allowance.minus(amount);
610+
(await this.token.allowance(owner, spender)).should.be.bignumber.equal(expectedAllowance);
611+
});
612+
613+
it('emits Transfer event', async function () {
614+
const event = expectEvent.inLogs(this.logs, 'Transfer', {
615+
from: owner,
616+
to: ZERO_ADDRESS,
617+
});
618+
619+
event.args.value.should.be.bignumber.equal(amount);
620+
});
621+
});
622+
});
623+
});
489624
});

0 commit comments

Comments
 (0)