diff --git a/contracts/mocks/ERC827TokenMock.sol b/contracts/mocks/ERC827TokenMock.sol new file mode 100644 index 000000000..8408d80e0 --- /dev/null +++ b/contracts/mocks/ERC827TokenMock.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.4.13; + + +import "../token/ERC827Token.sol"; + + +// mock class using ERC827 Token +contract ERC827TokenMock is ERC827Token { + + function ERC827TokenMock(address initialAccount, uint256 initialBalance) public { + balances[initialAccount] = initialBalance; + totalSupply = initialBalance; + } + +} diff --git a/contracts/mocks/MessageHelper.sol b/contracts/mocks/MessageHelper.sol new file mode 100644 index 000000000..f990241da --- /dev/null +++ b/contracts/mocks/MessageHelper.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.4.11; + +contract MessageHelper { + + event Show(bytes32 b32, uint256 number, string text); + + function showMessage( bytes32 message, uint256 number, string text ) public returns (bool) { + Show(message, number, text); + return true; + } + + function fail() public { + require(false); + } + + function call(address to, bytes data) public returns (bool) { + if (to.call(data)) + return true; + else + return false; + } + +} diff --git a/contracts/token/ERC827.sol b/contracts/token/ERC827.sol new file mode 100644 index 000000000..f0793e607 --- /dev/null +++ b/contracts/token/ERC827.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.4.13; + + +import "./ERC20.sol"; + + +/** + @title ERC827 interface, an extension of ERC20 token standard + + Interface of a ERC827 token, following the ERC20 standard with extra + methods to transfer value and data and execute calls in transfers and + approvals. + */ +contract ERC827 is ERC20 { + + function approve( address _spender, uint256 _value, bytes _data ) public returns (bool); + function transfer( address _to, uint256 _value, bytes _data ) public returns (bool); + function transferFrom( address _from, address _to, uint256 _value, bytes _data ) public returns (bool); + +} diff --git a/contracts/token/ERC827Token.sol b/contracts/token/ERC827Token.sol new file mode 100644 index 000000000..173acf98f --- /dev/null +++ b/contracts/token/ERC827Token.sol @@ -0,0 +1,126 @@ +pragma solidity ^0.4.13; + +import "./ERC827.sol"; +import "./StandardToken.sol"; + +/** + @title ERC827, an extension of ERC20 token standard + + Implementation the ERC827, following the ERC20 standard with extra + methods to transfer value and data and execute calls in transfers and + approvals. + Uses OpenZeppelin StandardToken. + */ +contract ERC827Token is ERC827, StandardToken { + + /** + @dev Addition to ERC20 token methods. It allows to + approve the transfer of value and execute a call with the sent data. + + Beware that changing an allowance with this method brings the risk that + someone may use both the old and the new allowance by unfortunate + transaction ordering. One possible solution to mitigate this race condition + is to first reduce the spender's allowance to 0 and set the desired value + afterwards: + https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + + @param _spender The address that will spend the funds. + @param _value The amount of tokens to be spent. + @param _data ABI-encoded contract call to call `_to` address. + + @return true if the call function was executed successfully + */ + function approve(address _spender, uint256 _value, bytes _data) public returns (bool) { + require(_spender != address(this)); + + super.approve(_spender, _value); + + require(_spender.call(_data)); + + return true; + } + + /** + @dev Addition to ERC20 token methods. Transfer tokens to a specified + address and execute a call with the sent data on the same transaction + + @param _to address The address which you want to transfer to + @param _value uint256 the amout of tokens to be transfered + @param _data ABI-encoded contract call to call `_to` address. + + @return true if the call function was executed successfully + */ + function transfer(address _to, uint256 _value, bytes _data) public returns (bool) { + require(_to != address(this)); + + super.transfer(_to, _value); + + require(_to.call(_data)); + return true; + } + + /** + @dev Addition to ERC20 token methods. Transfer tokens from one address to + another and make a contract call on the same transaction + + @param _from The address which you want to send tokens from + @param _to The address which you want to transfer to + @param _value The amout of tokens to be transferred + @param _data ABI-encoded contract call to call `_to` address. + + @return true if the call function was executed successfully + */ + function transferFrom(address _from, address _to, uint256 _value, bytes _data) public returns (bool) { + require(_to != address(this)); + + super.transferFrom(_from, _to, _value); + + require(_to.call(_data)); + return true; + } + + /** + * @dev Addition to StandardToken methods. Increase the amount of tokens that + * an owner allowed to a spender and execute a call with the sent data. + * + * approve should be called when allowed[_spender] == 0. To increment + * allowed value is better to use this function to avoid 2 calls (and wait until + * the first transaction is mined) + * From MonolithDAO Token.sol + * @param _spender The address which will spend the funds. + * @param _addedValue The amount of tokens to increase the allowance by. + * @param _data ABI-encoded contract call to call `_spender` address. + */ + function increaseApproval(address _spender, uint _addedValue, bytes _data) public returns (bool) { + require(_spender != address(this)); + + super.increaseApproval(_spender, _addedValue); + + require(_spender.call(_data)); + + return true; + } + + /** + * @dev Addition to StandardToken methods. Decrease the amount of tokens that + * an owner allowed to a spender and execute a call with the sent data. + * + * approve should be called when allowed[_spender] == 0. To decrement + * allowed value is better to use this function to avoid 2 calls (and wait until + * the first transaction is mined) + * From MonolithDAO Token.sol + * @param _spender The address which will spend the funds. + * @param _subtractedValue The amount of tokens to decrease the allowance by. + * @param _data ABI-encoded contract call to call `_spender` address. + */ + function decreaseApproval(address _spender, uint _subtractedValue, bytes _data) public returns (bool) { + require(_spender != address(this)); + + super.decreaseApproval(_spender, _subtractedValue); + + require(_spender.call(_data)); + + return true; + } + +} diff --git a/package.json b/package.json index f15f6397b..6d93e41e1 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "truffle-hdwallet-provider": "0.0.3" }, "dependencies": { - "dotenv": "^4.0.0" + "dotenv": "^4.0.0", + "ethjs-abi": "^0.2.1" } } diff --git a/test/ERC827Token.js b/test/ERC827Token.js new file mode 100644 index 000000000..a88540ba6 --- /dev/null +++ b/test/ERC827Token.js @@ -0,0 +1,374 @@ + +import EVMRevert from './helpers/EVMRevert'; +var Message = artifacts.require('./mock/MessageHelper.sol'); +var ERC827TokenMock = artifacts.require('./mock/ERC827TokenMock.sol'); + +var BigNumber = web3.BigNumber; +var _ = require('lodash'); +var ethjsABI = require('ethjs-abi'); +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +contract('ERC827 Token', function (accounts) { + let token; + + function findMethod (abi, name, args) { + for (var i = 0; i < abi.length; i++) { + const methodArgs = _.map(abi[i].inputs, 'type').join(','); + if ((abi[i].name === name) && (methodArgs === args)) { + return abi[i]; + } + } + } + + beforeEach(async function () { + token = await ERC827TokenMock.new(accounts[0], 100); + }); + + it('should return the correct totalSupply after construction', async function () { + let totalSupply = await token.totalSupply(); + + assert.equal(totalSupply, 100); + }); + + it('should return the correct allowance amount after approval', async function () { + let token = await ERC827TokenMock.new(); + await token.approve(accounts[1], 100); + let allowance = await token.allowance(accounts[0], accounts[1]); + + assert.equal(allowance, 100); + }); + + it('should return correct balances after transfer', async function () { + await token.transfer(accounts[1], 100); + let balance0 = await token.balanceOf(accounts[0]); + assert.equal(balance0, 0); + + let balance1 = await token.balanceOf(accounts[1]); + assert.equal(balance1, 100); + }); + + it('should throw an error when trying to transfer more than balance', async function () { + await token.transfer(accounts[1], 101).should.be.rejectedWith(EVMRevert); + }); + + it('should return correct balances after transfering from another account', async function () { + await token.approve(accounts[1], 100); + await token.transferFrom(accounts[0], accounts[2], 100, { from: accounts[1] }); + + let balance0 = await token.balanceOf(accounts[0]); + assert.equal(balance0, 0); + + let balance1 = await token.balanceOf(accounts[2]); + assert.equal(balance1, 100); + + let balance2 = await token.balanceOf(accounts[1]); + assert.equal(balance2, 0); + }); + + it('should throw an error when trying to transfer more than allowed', async function () { + await token.approve(accounts[1], 99); + await token.transferFrom( + accounts[0], accounts[2], 100, + { from: accounts[1] } + ).should.be.rejectedWith(EVMRevert); + }); + + it('should throw an error when trying to transferFrom more than _from has', async function () { + let balance0 = await token.balanceOf(accounts[0]); + await token.approve(accounts[1], 99); + await token.transferFrom( + accounts[0], accounts[2], balance0 + 1, + { from: accounts[1] } + ).should.be.rejectedWith(EVMRevert); + }); + + describe('validating allowance updates to spender', function () { + let preApproved; + + it('should start with zero', async function () { + preApproved = await token.allowance(accounts[0], accounts[1]); + assert.equal(preApproved, 0); + }); + + it('should increase by 50 then decrease by 10', async function () { + const abiMethod = findMethod(token.abi, 'increaseApproval', 'address,uint256'); + const increaseApprovalData = ethjsABI.encodeMethod(abiMethod, + [accounts[1], 50] + ); + await token.sendTransaction( + { from: accounts[0], data: increaseApprovalData } + ); + let postIncrease = await token.allowance(accounts[0], accounts[1]); + preApproved.plus(50).should.be.bignumber.equal(postIncrease); + await token.decreaseApproval(accounts[1], 10); + let postDecrease = await token.allowance(accounts[0], accounts[1]); + postIncrease.minus(10).should.be.bignumber.equal(postDecrease); + }); + }); + + it('should increase by 50 then set to 0 when decreasing by more than 50', async function () { + await token.approve(accounts[1], 50); + await token.decreaseApproval(accounts[1], 60); + let postDecrease = await token.allowance(accounts[0], accounts[1]); + postDecrease.should.be.bignumber.equal(0); + }); + + it('should throw an error when trying to transfer to 0x0', async function () { + await token.transfer(0x0, 100).should.be.rejectedWith(EVMRevert); + }); + + it('should throw an error when trying to transferFrom to 0x0', async function () { + await token.approve(accounts[1], 100); + await token.transferFrom(accounts[0], 0x0, 100, { from: accounts[1] }) + .should.be.rejectedWith(EVMRevert); + }); + + describe('Test ERC827 methods', function () { + it( + 'should return correct balances after transfer (with data) and show the event on receiver contract' + , async function () { + const message = await Message.new(); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + const abiMethod = findMethod(token.abi, 'transfer', 'address,uint256,bytes'); + const transferData = ethjsABI.encodeMethod(abiMethod, + [message.contract.address, 100, extraData] + ); + const transaction = await token.sendTransaction( + { from: accounts[0], data: transferData } + ); + + assert.equal(2, transaction.receipt.logs.length); + + new BigNumber(100).should.be.bignumber.equal( + await token.balanceOf(message.contract.address) + ); + }); + + it( + 'should return correct allowance after approve (with data) and show the event on receiver contract' + , async function () { + const message = await Message.new(); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + const abiMethod = findMethod(token.abi, 'approve', 'address,uint256,bytes'); + const approveData = ethjsABI.encodeMethod(abiMethod, + [message.contract.address, 100, extraData] + ); + const transaction = await token.sendTransaction( + { from: accounts[0], data: approveData } + ); + + assert.equal(2, transaction.receipt.logs.length); + + new BigNumber(100).should.be.bignumber.equal( + await token.allowance(accounts[0], message.contract.address) + ); + }); + + it( + 'should return correct allowance after increaseApproval (with data) and show the event on receiver contract' + , async function () { + const message = await Message.new(); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + await token.approve(message.contract.address, 10); + new BigNumber(10).should.be.bignumber.equal( + await token.allowance(accounts[0], message.contract.address) + ); + + const abiMethod = findMethod(token.abi, 'increaseApproval', 'address,uint256,bytes'); + const increaseApprovalData = ethjsABI.encodeMethod(abiMethod, + [message.contract.address, 50, extraData] + ); + const transaction = await token.sendTransaction( + { from: accounts[0], data: increaseApprovalData } + ); + + assert.equal(2, transaction.receipt.logs.length); + + new BigNumber(60).should.be.bignumber.equal( + await token.allowance(accounts[0], message.contract.address) + ); + }); + + it( + 'should return correct allowance after decreaseApproval (with data) and show the event on receiver contract' + , async function () { + const message = await Message.new(); + + await token.approve(message.contract.address, 100); + + new BigNumber(100).should.be.bignumber.equal( + await token.allowance(accounts[0], message.contract.address) + ); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + const abiMethod = findMethod(token.abi, 'decreaseApproval', 'address,uint256,bytes'); + const decreaseApprovalData = ethjsABI.encodeMethod(abiMethod, + [message.contract.address, 60, extraData] + ); + const transaction = await token.sendTransaction( + { from: accounts[0], data: decreaseApprovalData } + ); + + assert.equal(2, transaction.receipt.logs.length); + + new BigNumber(40).should.be.bignumber.equal( + await token.allowance(accounts[0], message.contract.address) + ); + }); + + it( + 'should return correct balances after transferFrom (with data) and show the event on receiver contract' + , async function () { + const message = await Message.new(); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + await token.approve(accounts[1], 100, { from: accounts[0] }); + + new BigNumber(100).should.be.bignumber.equal( + await token.allowance(accounts[0], accounts[1]) + ); + + const abiMethod = findMethod(token.abi, 'transferFrom', 'address,address,uint256,bytes'); + const transferFromData = ethjsABI.encodeMethod(abiMethod, + [accounts[0], message.contract.address, 100, extraData] + ); + const transaction = await token.sendTransaction( + { from: accounts[1], data: transferFromData } + ); + + assert.equal(2, transaction.receipt.logs.length); + + new BigNumber(100).should.be.bignumber.equal( + await token.balanceOf(message.contract.address) + ); + }); + + it('should fail inside approve (with data)', async function () { + const message = await Message.new(); + + const extraData = message.contract.fail.getData(); + + const abiMethod = findMethod(token.abi, 'approve', 'address,uint256,bytes'); + const approveData = ethjsABI.encodeMethod(abiMethod, + [message.contract.address, 10, extraData] + ); + await token.sendTransaction( + { from: accounts[0], data: approveData } + ).should.be.rejectedWith(EVMRevert); + + // approval should not have gone through so allowance is still 0 + new BigNumber(0).should.be.bignumber + .equal(await token.allowance(accounts[1], message.contract.address)); + }); + + it('should fail inside transfer (with data)', async function () { + const message = await Message.new(); + + const extraData = message.contract.fail.getData(); + + const abiMethod = findMethod(token.abi, 'transfer', 'address,uint256,bytes'); + const transferData = ethjsABI.encodeMethod(abiMethod, + [message.contract.address, 10, extraData] + ); + await token.sendTransaction( + { from: accounts[0], data: transferData } + ).should.be.rejectedWith(EVMRevert); + + // transfer should not have gone through, so balance is still 0 + new BigNumber(0).should.be.bignumber + .equal(await token.balanceOf(message.contract.address)); + }); + + it('should fail inside transferFrom (with data)', async function () { + const message = await Message.new(); + + const extraData = message.contract.fail.getData(); + + await token.approve(accounts[1], 10, { from: accounts[2] }); + + const abiMethod = findMethod(token.abi, 'transferFrom', 'address,address,uint256,bytes'); + const transferFromData = ethjsABI.encodeMethod(abiMethod, + [accounts[2], message.contract.address, 10, extraData] + ); + await token.sendTransaction( + { from: accounts[1], data: transferFromData } + ).should.be.rejectedWith(EVMRevert); + + // transferFrom should have failed so balance is still 0 but allowance is 10 + new BigNumber(10).should.be.bignumber + .equal(await token.allowance(accounts[2], accounts[1])); + new BigNumber(0).should.be.bignumber + .equal(await token.balanceOf(message.contract.address)); + }); + + it('should fail approve (with data) when using token contract address as receiver', async function () { + const message = await Message.new(); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + const abiMethod = findMethod(token.abi, 'approve', 'address,uint256,bytes'); + const approveData = ethjsABI.encodeMethod(abiMethod, + [token.contract.address, 100, extraData] + ); + await token.sendTransaction( + { from: accounts[0], data: approveData } + ).should.be.rejectedWith(EVMRevert); + }); + + it('should fail transfer (with data) when using token contract address as receiver', async function () { + const message = await Message.new(); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + const abiMethod = findMethod(token.abi, 'transfer', 'address,uint256,bytes'); + const transferData = ethjsABI.encodeMethod(abiMethod, + [token.contract.address, 100, extraData] + ); + await token.sendTransaction( + { from: accounts[0], data: transferData } + ).should.be.rejectedWith(EVMRevert); + }); + + it('should fail transferFrom (with data) when using token contract address as receiver', async function () { + const message = await Message.new(); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + await token.approve(accounts[1], 1, { from: accounts[0] }); + + const abiMethod = findMethod(token.abi, 'transferFrom', 'address,address,uint256,bytes'); + const transferFromData = ethjsABI.encodeMethod(abiMethod, + [accounts[0], token.contract.address, 1, extraData] + ); + await token.sendTransaction( + { from: accounts[1], data: transferFromData } + ).should.be.rejectedWith(EVMRevert); + }); + }); +});