瀏覽代碼

Add StandardBurnableToken implementation (#870)

* Add StandardBurnableToken implementation

BurnableToken that extends from StandardToken and adds a
burnFrom method that decrements allowance. Equivalent to
a transferFrom plus burn in a single operation.

* Return event object from expectEvent helper

* Add comment on Approval event in burnFrom function

* Improvements on burnable token tests

- Inject initial balance as a parameter to the behaviour
- Use expectEvent helper for assertions on events
- Use chai bignumber for numbers
- Change to bdd-style assertions
Santiago Palladino 7 年之前
父節點
當前提交
0926729c8f

+ 13 - 0
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;
+  }
+
+}

+ 8 - 5
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);
   }
 }

+ 24 - 0
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);
+  }
+}

+ 1 - 0
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) => {

+ 50 - 0
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 }));
+      });
+    });
+  });
+};

+ 5 - 38
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);
 });

+ 73 - 0
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 }));
+      });
+    });
+  });
+});