From 86590de65ac3232a807b6599ceb5eae4db5c6197 Mon Sep 17 00:00:00 2001 From: AugustoL Date: Tue, 17 Jul 2018 12:21:07 +0200 Subject: [PATCH] Add ERC827 with proxy proposal --- .../proposals/ERC827AllowedCallbacksProxy.sol | 71 +++ contracts/ERC827/proposals/ERC827Proxy.sol | 55 +- .../ERC827/proposals/ERC827TokenWithProxy.sol | 192 +++++++ ...RC827Proxy.js => AllowedCallbacksProxy.js} | 6 +- test/proposals/ERC827TokenWithProxy.js | 484 ++++++++++++++++++ 5 files changed, 767 insertions(+), 41 deletions(-) create mode 100644 contracts/ERC827/proposals/ERC827AllowedCallbacksProxy.sol create mode 100644 contracts/ERC827/proposals/ERC827TokenWithProxy.sol rename test/proposals/{ERC827Proxy.js => AllowedCallbacksProxy.js} (84%) create mode 100644 test/proposals/ERC827TokenWithProxy.js diff --git a/contracts/ERC827/proposals/ERC827AllowedCallbacksProxy.sol b/contracts/ERC827/proposals/ERC827AllowedCallbacksProxy.sol new file mode 100644 index 0000000..16172a2 --- /dev/null +++ b/contracts/ERC827/proposals/ERC827AllowedCallbacksProxy.sol @@ -0,0 +1,71 @@ +/* solium-disable security/no-low-level-calls */ + +pragma solidity ^0.4.24; + +import "./ERC827TokenMockAllowedCallbacks.sol"; + +/** + * @title ERC827AllowedCallbacksProxy + * + * @dev Proxy to forward tokens balance and allowance with arbitrary calls + */ +contract ERC827AllowedCallbacksProxy { + + ERC827TokenAllowedCallbacks public token; + + /** + * @dev Constructor + */ + constructor(ERC827TokenAllowedCallbacks _token) public { + token = _token; + bytes4 makeCallSig = bytes4(keccak256('makeCall(address,bytes)')); + token.allowCallback(address(0), makeCallSig, + ERC827TokenAllowedCallbacks.FunctionType.Approve + ); + token.allowCallback(address(0), makeCallSig, + ERC827TokenAllowedCallbacks.FunctionType.Transfer + ); + token.allowCallback(address(0), makeCallSig, + ERC827TokenAllowedCallbacks.FunctionType.TransferFrom + ); + } + + /** + * @dev Fallback function that give back all tokens received + */ + function() { + forwardTokens(msg.sender); + } + + /** + * @dev Forward arbitary calls with token balance or allowance + * @param _target address The address which you want to transfer to + * @param _data bytes The data to be executed in the call + */ + function makeCall(address _target, bytes _data) public returns (bool) { + require(msg.sender == address(token)); + + forwardTokens(_target); + + // solium-disable-next-line security/no-call-value + return _target.call.value(msg.value)(_data); + } + + /** + * @dev Give back all tokens balance and allowance to address + * @param to address The address which you want to transfer to + */ + function forwardTokens(address to) internal { + uint256 callerBalance = token.balanceOf(address(this)); + uint256 callerAllowance = token.allowance(to, address(this)); + + // Give back token balance + if (callerBalance > 0) + token.transfer(to, callerBalance); + + // Give back token allowance + if (callerAllowance > 0) + token.transferFrom(address(this), to, callerAllowance); + } + +} diff --git a/contracts/ERC827/proposals/ERC827Proxy.sol b/contracts/ERC827/proposals/ERC827Proxy.sol index 7ad017d..ac73890 100644 --- a/contracts/ERC827/proposals/ERC827Proxy.sol +++ b/contracts/ERC827/proposals/ERC827Proxy.sol @@ -2,7 +2,7 @@ pragma solidity ^0.4.24; -import "./ERC827TokenMockAllowedCallbacks.sol"; +import "../ERC827.sol"; /** * @title ERC827Proxy @@ -11,30 +11,17 @@ import "./ERC827TokenMockAllowedCallbacks.sol"; */ contract ERC827Proxy { - ERC827TokenAllowedCallbacks public token; + ERC827 public token; + bytes4 public makeCallSig = bytes4(keccak256('makeCall(address,bytes)')); /** - * @dev Constructor + * @dev Set the token address, can be called only once + * @param _token The ERC827 token to be used for the proxy */ - constructor(ERC827TokenAllowedCallbacks _token) public { + function setToken(ERC827 _token) public { + require(token == address(0)); + require(_token != address(0)); token = _token; - bytes4 makeCallSig = bytes4(keccak256('makeCall(address,bytes)')); - token.allowCallback(address(0), makeCallSig, - ERC827TokenAllowedCallbacks.FunctionType.Approve - ); - token.allowCallback(address(0), makeCallSig, - ERC827TokenAllowedCallbacks.FunctionType.Transfer - ); - token.allowCallback(address(0), makeCallSig, - ERC827TokenAllowedCallbacks.FunctionType.TransferFrom - ); - } - - /** - * @dev Fallback function that give back all tokens received - */ - function() { - forwardTokens(msg.sender); } /** @@ -42,30 +29,22 @@ contract ERC827Proxy { * @param _target address The address which you want to transfer to * @param _data bytes The data to be executed in the call */ - function makeCall(address _target, bytes _data) public returns (bool) { + function makeCall(address _target, bytes _data) payable public returns (bool) { require(msg.sender == address(token)); - forwardTokens(_target); - - // solium-disable-next-line security/no-call-value - return _target.call.value(msg.value)(_data); - } - - /** - * @dev Give back all tokens balance and allowance to address - * @param to address The address which you want to transfer to - */ - function forwardTokens(address to) internal { uint256 callerBalance = token.balanceOf(address(this)); - uint256 callerAllowance = token.allowance(to, address(this)); + uint256 callerAllowance = token.allowance(_target, address(this)); - // Give back token balance + // Transfer token balance if (callerBalance > 0) - token.transfer(to, callerBalance); + token.transfer(_target, callerBalance); - // Give back token allowance + // Transfer token allowance if (callerAllowance > 0) - token.transferFrom(address(this), to, callerAllowance); + token.transferFrom(address(this), _target, callerAllowance); + + // solium-disable-next-line security/no-call-value + require(_target.call.value(msg.value)(_data)); } } diff --git a/contracts/ERC827/proposals/ERC827TokenWithProxy.sol b/contracts/ERC827/proposals/ERC827TokenWithProxy.sol new file mode 100644 index 0000000..0200603 --- /dev/null +++ b/contracts/ERC827/proposals/ERC827TokenWithProxy.sol @@ -0,0 +1,192 @@ +/* solium-disable security/no-low-level-calls */ + +pragma solidity ^0.4.24; + +import "../ERC827.sol"; +import "../../ERC20/StandardToken.sol"; +import "./ERC827Proxy.sol"; + +/** + * @title ERC827, an extension of ERC20 token standard + * + * @dev 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 and ERC827Proxy. + */ +contract ERC827TokenWithProxy is ERC827, StandardToken { + + ERC827Proxy public proxy; + + /** + * @dev Constructor + * @param _proxy The address of the ERC827 proxy to be used + */ + constructor(ERC827Proxy _proxy) public { + proxy = _proxy; + } + + /** + * @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 `_spender` address. + * @return true if the call function was executed successfully + */ + function approveAndCall( + address _spender, + uint256 _value, + bytes _data + ) + public + payable + returns (bool) + { + require(_spender != address(this)); + + super.approve(_spender, _value); + + // solium-disable-next-line security/no-call-value + require(address(proxy).call.value(msg.value)( + abi.encodeWithSelector(proxy.makeCallSig(), _spender, _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 transferAndCall( + address _to, + uint256 _value, + bytes _data + ) + public + payable + returns (bool) + { + require(_to != address(this)); + + super.transfer(_to, _value); + + // solium-disable-next-line security/no-call-value + require(address(proxy).call.value(msg.value)( + abi.encodeWithSelector(proxy.makeCallSig(), _to, _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 transferFromAndCall( + address _from, + address _to, + uint256 _value, + bytes _data + ) + public payable returns (bool) + { + require(_to != address(this)); + + super.transferFrom(_from, _to, _value); + + // solium-disable-next-line security/no-call-value + require(address(proxy).call.value(msg.value)( + abi.encodeWithSelector(proxy.makeCallSig(), _to, _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 increaseApprovalAndCall( + address _spender, + uint _addedValue, + bytes _data + ) + public + payable + returns (bool) + { + require(_spender != address(this)); + + super.increaseApproval(_spender, _addedValue); + + // solium-disable-next-line security/no-call-value + require(address(proxy).call.value(msg.value)( + abi.encodeWithSelector(proxy.makeCallSig(), _spender, _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 decreaseApprovalAndCall( + address _spender, + uint _subtractedValue, + bytes _data + ) + public + payable + returns (bool) + { + require(_spender != address(this)); + + super.decreaseApproval(_spender, _subtractedValue); + + // solium-disable-next-line security/no-call-value + require(address(proxy).call.value(msg.value)( + abi.encodeWithSelector(proxy.makeCallSig(), _spender, _data)) + ); + return true; + } + +} + +// mock class using ERC827 Token with proxy +contract ERC827TokenWithProxyMock is ERC827TokenWithProxy { + + constructor( + address initialAccount, uint256 initialBalance, ERC827Proxy proxy + ) ERC827TokenWithProxy(proxy) public { + balances[initialAccount] = initialBalance; + totalSupply_ = initialBalance; + } + +} diff --git a/test/proposals/ERC827Proxy.js b/test/proposals/AllowedCallbacksProxy.js similarity index 84% rename from test/proposals/ERC827Proxy.js rename to test/proposals/AllowedCallbacksProxy.js index 910c4a0..75037e2 100644 --- a/test/proposals/ERC827Proxy.js +++ b/test/proposals/AllowedCallbacksProxy.js @@ -2,7 +2,7 @@ import EVMRevert from '../helpers/EVMRevert'; var Message = artifacts.require('./mocks/MessageHelper'); var ERC827TokenMock = artifacts.require('./ERC827/proposals/ERC827TokenMockAllowedCallbacks'); -var ERC827Proxy = artifacts.require('./ERC827/proposals/ERC827Proxy'); +var ERC827AllowedCallbacksProxy = artifacts.require('./ERC827/proposals/ERC827AllowedCallbacksProxy'); var BigNumber = web3.BigNumber; require('chai') @@ -23,8 +23,8 @@ contract('ERC827 Proxy for allowed callbacks', function (accounts) { token = await ERC827TokenMock.new(accounts[0], 100); }); - it('should forward token balance correctly with ERC827Proxy', async function () { - let proxy = await ERC827Proxy.new(token.address); + it('should forward token balance correctly with ERC827AllowedCallbacksProxy', async function () { + let proxy = await ERC827AllowedCallbacksProxy.new(token.address); let makeCallData = proxy.contract.makeCall.getData(message.address, messageData); let makeCallSig = makeCallData.substring(0, 10); diff --git a/test/proposals/ERC827TokenWithProxy.js b/test/proposals/ERC827TokenWithProxy.js new file mode 100644 index 0000000..5321f7e --- /dev/null +++ b/test/proposals/ERC827TokenWithProxy.js @@ -0,0 +1,484 @@ + +import EVMRevert from '../helpers/EVMRevert'; +var Message = artifacts.require('MessageHelper'); +var ERC827Proxy = artifacts.require('ERC827Proxy'); +var ERC827TokenWithProxyMock = artifacts.require('ERC827TokenWithProxyMock'); + +var BigNumber = web3.BigNumber; +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +contract('ERC827 Token with Proxy', function (accounts) { + let token, proxy; + + beforeEach(async function () { + proxy = await ERC827Proxy.new({gasPrice: 0}); + token = await ERC827TokenWithProxyMock.new(accounts[0], 100, proxy.address); + await proxy.setToken(token.address); + }); + + it('should return the correct totalSupply after construction', async function () { + let totalSupply = await token.totalSupply(); + + assert.equal(totalSupply, 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 () { + await token.increaseApproval(accounts[1], 50); + 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 allow payment through transfer' + , async function () { + const message = await Message.new(); + + const extraData = message.contract.buyMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + const transaction = await token.transferAndCall( + message.contract.address, 100, extraData, { from: accounts[0], value: 1000 } + ); + + assert.equal(2, transaction.receipt.logs.length); + + new BigNumber(100).should.be.bignumber.equal( + await token.balanceOf(message.contract.address) + ); + new BigNumber(1000).should.be.bignumber.equal( + await web3.eth.getBalance(message.contract.address) + ); + }); + + it( + 'should allow payment through approve' + , async function () { + const message = await Message.new(); + + const extraData = message.contract.buyMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + const transaction = await token.approveAndCall( + message.contract.address, 100, extraData, { from: accounts[0], value: 1000 } + ); + + assert.equal(2, transaction.receipt.logs.length); + + new BigNumber(100).should.be.bignumber.equal( + await token.allowance(accounts[0], message.contract.address) + ); + new BigNumber(1000).should.be.bignumber.equal( + await web3.eth.getBalance(message.contract.address) + ); + }); + + it( + 'should allow payment through increaseApproval' + , async function () { + const message = await Message.new(); + + const extraData = message.contract.buyMessage.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 transaction = await token.increaseApprovalAndCall( + message.contract.address, 50, extraData, { from: accounts[0], value: 1000 } + ); + + assert.equal(2, transaction.receipt.logs.length); + + new BigNumber(60).should.be.bignumber.equal( + await token.allowance(accounts[0], message.contract.address) + ); + new BigNumber(1000).should.be.bignumber.equal( + await web3.eth.getBalance(message.contract.address) + ); + }); + + it( + 'should allow payment through decreaseApproval' + , 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.buyMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + const transaction = await token.decreaseApprovalAndCall( + message.contract.address, 60, extraData, { from: accounts[0], value: 1000 } + ); + + assert.equal(2, transaction.receipt.logs.length); + + new BigNumber(40).should.be.bignumber.equal( + await token.allowance(accounts[0], message.contract.address) + ); + new BigNumber(1000).should.be.bignumber.equal( + await web3.eth.getBalance(message.contract.address) + ); + }); + + it( + 'should allow payment through transferFrom' + , async function () { + const message = await Message.new(); + + const extraData = message.contract.buyMessage.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 transaction = await token.transferFromAndCall( + accounts[0], message.contract.address, 100, extraData, { from: accounts[1], value: 1000 } + ); + + assert.equal(2, transaction.receipt.logs.length); + + new BigNumber(100).should.be.bignumber.equal( + await token.balanceOf(message.contract.address) + ); + new BigNumber(1000).should.be.bignumber.equal( + await web3.eth.getBalance(message.contract.address) + ); + }); + + it('should revert funds of failure inside approve (with data)', async function () { + const message = await Message.new(); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + await token.approveAndCall( + message.contract.address, 10, extraData, { from: accounts[0], value: 1000 } + ).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)); + new BigNumber(0).should.be.bignumber + .equal(await web3.eth.getBalance(message.contract.address)); + }); + + it('should revert funds of failure inside transfer (with data)', async function () { + const message = await Message.new(); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + await token.transferAndCall( + message.contract.address, 10, extraData, { from: accounts[0], value: 1000 } + ).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)); + new BigNumber(0).should.be.bignumber + .equal(await web3.eth.getBalance(message.contract.address)); + }); + + it('should revert funds of failure inside transferFrom (with data)', async function () { + const message = await Message.new(); + + const extraData = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + await token.approve(accounts[1], 10, { from: accounts[2] }); + + await token.transferFromAndCall( + accounts[2], message.contract.address, 10, extraData, { from: accounts[2], value: 1000 } + ).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)); + new BigNumber(0).should.be.bignumber + .equal(await web3.eth.getBalance(message.contract.address)); + }); + + 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 transaction = await token.transferAndCall(message.contract.address, 100, extraData); + + 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 transaction = await token.approveAndCall(message.contract.address, 100, extraData); + + 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 transaction = await token.increaseApprovalAndCall(message.contract.address, 50, extraData); + + 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 transaction = await token.decreaseApprovalAndCall(message.contract.address, 60, extraData); + + 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 transaction = await token.transferFromAndCall(accounts[0], message.contract.address, 100, extraData, { + from: accounts[1], + }); + + 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(); + + await token.approveAndCall(message.contract.address, 10, extraData) + .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(); + + await token.transferAndCall(message.contract.address, 10, extraData) + .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] }); + await token.transferFromAndCall(accounts[2], message.contract.address, 10, extraData, { from: accounts[1] }) + .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' + ); + + await token.approveAndCall(token.contract.address, 100, extraData, { from: accounts[0] }) + .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' + ); + + await token.transferAndCall(token.contract.address, 100, extraData) + .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] }); + + await token.transferFromAndCall(accounts[0], token.contract.address, 1, extraData, { from: accounts[1] }) + .should.be.rejectedWith(EVMRevert); + }); + }); +});