diff --git a/contracts/mocks/StandardBurnableTokenMock.sol b/contracts/mocks/StandardBurnableTokenMock.sol new file mode 100644 index 000000000..e2ccddde2 --- /dev/null +++ b/contracts/mocks/StandardBurnableTokenMock.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.4.18; + +import "../token/ERC20/StandardBurnableToken.sol"; + + +contract StandardBurnableTokenMock is StandardBurnableToken { + + function StandardBurnableTokenMock(address initialAccount, uint initialBalance) public { + balances[initialAccount] = initialBalance; + totalSupply_ = initialBalance; + } + +} diff --git a/contracts/token/ERC20/BurnableToken.sol b/contracts/token/ERC20/BurnableToken.sol index 5062e9c4e..9cd9b5506 100644 --- a/contracts/token/ERC20/BurnableToken.sol +++ b/contracts/token/ERC20/BurnableToken.sol @@ -16,14 +16,17 @@ contract BurnableToken is BasicToken { * @param _value The amount of token to be burned. */ function burn(uint256 _value) public { - require(_value <= balances[msg.sender]); + _burn(msg.sender, _value); + } + + function _burn(address _who, uint256 _value) internal { + require(_value <= balances[_who]); // no need to require value <= totalSupply, since that would imply the // sender's balance is greater than the totalSupply, which *should* be an assertion failure - address burner = msg.sender; - balances[burner] = balances[burner].sub(_value); + balances[_who] = balances[_who].sub(_value); totalSupply_ = totalSupply_.sub(_value); - emit Burn(burner, _value); - emit Transfer(burner, address(0), _value); + emit Burn(_who, _value); + emit Transfer(_who, address(0), _value); } } diff --git a/contracts/token/ERC20/StandardBurnableToken.sol b/contracts/token/ERC20/StandardBurnableToken.sol new file mode 100644 index 000000000..3a337331e --- /dev/null +++ b/contracts/token/ERC20/StandardBurnableToken.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.4.18; + +import "./BurnableToken.sol"; +import "./StandardToken.sol"; + +/** + * @title Standard Burnable Token + * @dev Adds burnFrom method to ERC20 implementations + */ +contract StandardBurnableToken is BurnableToken, StandardToken { + + /** + * @dev Burns a specific amount of tokens from the target address and decrements allowance + * @param _from address The address which you want to send tokens from + * @param _value uint256 The amount of token to be burned + */ + function burnFrom(address _from, uint256 _value) public { + require(_value <= allowed[_from][msg.sender]); + // Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted, + // this function needs to emit an event with the updated approval. + allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); + _burn(_from, _value); + } +} diff --git a/test/helpers/expectEvent.js b/test/helpers/expectEvent.js index 8a9502c36..72f91b7b9 100644 --- a/test/helpers/expectEvent.js +++ b/test/helpers/expectEvent.js @@ -3,6 +3,7 @@ const assert = require('chai').assert; const inLogs = async (logs, eventName) => { const event = logs.find(e => e.event === eventName); assert.exists(event); + return event; }; const inTransaction = async (tx, eventName) => { diff --git a/test/token/ERC20/BurnableToken.behaviour.js b/test/token/ERC20/BurnableToken.behaviour.js new file mode 100644 index 000000000..c3793bc1a --- /dev/null +++ b/test/token/ERC20/BurnableToken.behaviour.js @@ -0,0 +1,50 @@ +import assertRevert from '../../helpers/assertRevert'; +import { inLogs } from '../../helpers/expectEvent'; + +const BigNumber = web3.BigNumber; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +export default function ([owner], initialBalance) { + describe('as a basic burnable token', function () { + const from = owner; + + describe('when the given amount is not greater than balance of the sender', function () { + const amount = 100; + + beforeEach(async function () { + ({ logs: this.logs } = await this.token.burn(amount, { from })); + }); + + it('burns the requested amount', async function () { + const balance = await this.token.balanceOf(from); + balance.should.be.bignumber.equal(initialBalance - amount); + }); + + it('emits a burn event', async function () { + const event = await inLogs(this.logs, 'Burn'); + event.args.burner.should.eq(owner); + event.args.value.should.be.bignumber.equal(amount); + }); + + it('emits a transfer event', async function () { + const event = await inLogs(this.logs, 'Transfer'); + event.args.from.should.eq(owner); + event.args.to.should.eq(ZERO_ADDRESS); + event.args.value.should.be.bignumber.equal(amount); + }); + }); + + describe('when the given amount is greater than the balance of the sender', function () { + const amount = initialBalance + 1; + + it('reverts', async function () { + await assertRevert(this.token.burn(amount, { from })); + }); + }); + }); +}; diff --git a/test/token/ERC20/BurnableToken.test.js b/test/token/ERC20/BurnableToken.test.js index 51b7c2dd8..1b0c7bb30 100644 --- a/test/token/ERC20/BurnableToken.test.js +++ b/test/token/ERC20/BurnableToken.test.js @@ -1,45 +1,12 @@ -import assertRevert from '../../helpers/assertRevert'; +import shouldBehaveLikeBurnableToken from './BurnableToken.behaviour'; const BurnableTokenMock = artifacts.require('BurnableTokenMock'); contract('BurnableToken', function ([owner]) { + const initialBalance = 1000; + beforeEach(async function () { - this.token = await BurnableTokenMock.new(owner, 1000); + this.token = await BurnableTokenMock.new(owner, initialBalance); }); - describe('burn', function () { - const from = owner; - - describe('when the given amount is not greater than balance of the sender', function () { - const amount = 100; - - it('burns the requested amount', async function () { - await this.token.burn(amount, { from }); - - const balance = await this.token.balanceOf(from); - assert.equal(balance, 900); - }); - - it('emits a burn event', async function () { - const { logs } = await this.token.burn(amount, { from }); - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; - assert.equal(logs.length, 2); - assert.equal(logs[0].event, 'Burn'); - assert.equal(logs[0].args.burner, owner); - assert.equal(logs[0].args.value, amount); - - assert.equal(logs[1].event, 'Transfer'); - assert.equal(logs[1].args.from, owner); - assert.equal(logs[1].args.to, ZERO_ADDRESS); - assert.equal(logs[1].args.value, amount); - }); - }); - - describe('when the given amount is greater than the balance of the sender', function () { - const amount = 1001; - - it('reverts', async function () { - await assertRevert(this.token.burn(amount, { from })); - }); - }); - }); + shouldBehaveLikeBurnableToken([owner], initialBalance); }); diff --git a/test/token/ERC20/StandardBurnableToken.test.js b/test/token/ERC20/StandardBurnableToken.test.js new file mode 100644 index 000000000..f51438fcf --- /dev/null +++ b/test/token/ERC20/StandardBurnableToken.test.js @@ -0,0 +1,73 @@ +import assertRevert from '../../helpers/assertRevert'; +import { inLogs } from '../../helpers/expectEvent'; +import shouldBehaveLikeBurnableToken from './BurnableToken.behaviour'; + +const StandardBurnableTokenMock = artifacts.require('StandardBurnableTokenMock'); +const BigNumber = web3.BigNumber; +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +contract('StandardBurnableToken', function ([owner, burner]) { + const initialBalance = 1000; + + beforeEach(async function () { + this.token = await StandardBurnableTokenMock.new(owner, initialBalance); + }); + + shouldBehaveLikeBurnableToken([owner], initialBalance); + + describe('burnFrom', function () { + describe('on success', function () { + const amount = 100; + + beforeEach(async function () { + await this.token.approve(burner, 300, { from: owner }); + const { logs } = await this.token.burnFrom(owner, amount, { from: burner }); + this.logs = logs; + }); + + it('burns the requested amount', async function () { + const balance = await this.token.balanceOf(owner); + balance.should.be.bignumber.equal(initialBalance - amount); + }); + + it('decrements allowance', async function () { + const allowance = await this.token.allowance(owner, burner); + allowance.should.be.bignumber.equal(200); + }); + + it('emits a burn event', async function () { + const event = await inLogs(this.logs, 'Burn'); + event.args.burner.should.eq(owner); + event.args.value.should.be.bignumber.equal(amount); + }); + + it('emits a transfer event', async function () { + const event = await inLogs(this.logs, 'Transfer'); + event.args.from.should.eq(owner); + event.args.to.should.eq(ZERO_ADDRESS); + event.args.value.should.be.bignumber.equal(amount); + }); + }); + + describe('when the given amount is greater than the balance of the sender', function () { + const amount = initialBalance + 1; + it('reverts', async function () { + await this.token.approve(burner, amount, { from: owner }); + await assertRevert(this.token.burnFrom(owner, amount, { from: burner })); + }); + }); + + describe('when the given amount is greater than the allowance', function () { + const amount = 100; + it('reverts', async function () { + await this.token.approve(burner, amount - 1, { from: owner }); + await assertRevert(this.token.burnFrom(owner, amount, { from: burner })); + }); + }); + }); +});