diff --git a/contracts/mocks/MessageHelper.sol b/contracts/mocks/MessageHelper.sol new file mode 100644 index 000000000..5c864273c --- /dev/null +++ b/contracts/mocks/MessageHelper.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.4.11; + +contract MessageHelper { + + event Show(bytes32 b32, uint256 number, string text); + + function showMessage( + bytes32 message, uint256 number, string text + ) returns (bool) { + Show(message, number, text); + return true; + } + + function fail() { + throw; + } + + function call(address to, bytes data) returns (bool) { + if (to.call(data)) + return true; + else + return false; + } + +} diff --git a/contracts/mocks/SmartTokenMock.sol b/contracts/mocks/SmartTokenMock.sol new file mode 100644 index 000000000..37edbcee4 --- /dev/null +++ b/contracts/mocks/SmartTokenMock.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.4.13; + + +import '../token/SmartToken.sol'; + + +// mock class using SmartToken +contract SmartTokenMock is SmartToken { + + function SmartTokenMock(address initialAccount, uint256 initialBalance) { + balances[initialAccount] = initialBalance; + totalSupply = initialBalance; + } + +} diff --git a/contracts/token/SmartToken.sol b/contracts/token/SmartToken.sol new file mode 100644 index 000000000..cc3ced30f --- /dev/null +++ b/contracts/token/SmartToken.sol @@ -0,0 +1,81 @@ +pragma solidity ^0.4.13; + +import "./StandardToken.sol"; + +/** + @title SmartToken, an extension of ERC20 token standard + + Implementation the SmartToken, following the ERC20 standard with extra + methods to transfer value and data and execute calls in transfers and + approvals. + Uses OpenZeppelin StandardToken. + */ +contract SmartToken is StandardToken { + + /** + @dev `approveData` is an 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 approveData(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 transferData(address _to, uint256 _value, bytes _data) public returns (bool) { + require(_to != address(this)); + + require(_to.call(_data)); + + super.transfer(_to, _value); + 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 transferDataFrom(address _from, address _to, uint256 _value, bytes _data) public returns (bool) { + require(_to != address(this)); + + require(_to.call(_data)); + + super.transferFrom(_from, _to, _value); + return true; + } + +} diff --git a/docs/source/SmartToken b/docs/source/SmartToken new file mode 100644 index 000000000..016ef152b --- /dev/null +++ b/docs/source/SmartToken @@ -0,0 +1,16 @@ +SmartToken +============================================= + +Inherits from contract StandardToken. Implementation of a ERC20 compatible token with methods to transfer value and execute calls in transfers and approvals (see https://github.com/ethereum/EIPs/issues/20) + +approveData(address _spender, uint _value, bytes _data) returns (bool success) +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" +It allows to approve the transfer of value and execute a call with the sent data. + +function transferData(address _to, uint _value, bytes _data) returns (bool success) +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" +Transfer tokens to a specified address and execute a call with the sent data on the same transaction + +transferDataFrom(address _from, address _to, uint _value, bytes _data) returns (bool success) +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" +Transfer tokens from one address to another and make a contract call on the same transaction \ No newline at end of file diff --git a/test/SmartToken.js b/test/SmartToken.js new file mode 100644 index 000000000..2fd063c75 --- /dev/null +++ b/test/SmartToken.js @@ -0,0 +1,322 @@ +'use strict'; + +const assertJump = require('./helpers/assertJump'); +const expectThrow = require('./helpers/expectThrow'); +var Message = artifacts.require('./helpers/Message.sol'); +var SmartTokenMock = artifacts.require('./helpers/SmartTokenMock.sol'); + +var BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-bignumber')(BigNumber)) + .should(); + +contract('SmartToken', function(accounts) { + + let token; + + beforeEach(async function() { + token = await SmartTokenMock.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 SmartTokenMock.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() { + try { + await token.transfer(accounts[1], 101); + assert.fail('should have thrown before'); + } catch(error) { + assertJump(error); + } + }); + + 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); + try { + await token.transferFrom(accounts[0], accounts[2], 100, {from: accounts[1]}); + assert.fail('should have thrown before'); + } catch (error) { + assertJump(error); + } + }); + + 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); + try { + await token.transferFrom(accounts[0], accounts[2], balance0+1, {from: accounts[1]}); + assert.fail('should have thrown before'); + } catch (error) { + assertJump(error); + } + }); + + 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() { + try { + let transfer = await token.transfer(0x0, 100); + assert.fail('should have thrown before'); + } catch(error) { + assertJump(error); + } + }); + + it('should throw an error when trying to transferFrom to 0x0', async function() { + await token.approve(accounts[1], 100); + try { + let transfer = await token.transferFrom(accounts[0], 0x0, 100, {from: accounts[1]}); + assert.fail('should have thrown before'); + } catch(error) { + assertJump(error); + } + }); + + it('should return correct balances after approve and show the event on receiver contract', async function() { + let message = await Message.new(); + + let data = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + let transaction = await token.approveData( + message.contract.address, 100, data, {from: accounts[0]} + ); + + 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 balances after transferData and show the event on receiver contract', async function() { + let message = await Message.new(); + + let data = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + let transaction = await token.transferData( + message.contract.address, 100, data, {from: accounts[0]} + ); + + 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 approveData and show the event on receiver contract', async function() { + let message = await Message.new(); + + let data = message.contract.showMessage.getData( + web3.toHex(123456), 666, 'Transfer Done' + ); + + let transaction = await token.approveData( + message.contract.address, 100, data, {from: accounts[0]} + ); + + 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 balances after transferFrom and show the event on receiver contract', async function() { + let message = await Message.new(); + + let data = 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]) + ); + + let transaction = await token.transferDataFrom( + accounts[0], message.contract.address, 100, data, {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 approveData', async function() { + let message = await Message.new(); + + let data = message.contract.fail.getData(); + + try { + await token.approveData( + message.contract.address, 10, data, + {from: accounts[1]} + ); + assert(false, 'approveData should have raised'); + } catch(error) { + assertJump(error); + } + + // 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 transferData', async function() { + let message = await Message.new(); + + let data = message.contract.fail.getData(); + + try { + await token.transferData( + message.contract.address, 10, data, + {from: accounts[0]} + ); + assert(false, 'transferData should have failed'); + } catch(error) { + assertJump(error); + } + + // 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 transferDataFrom', async function() { + let message = await Message.new(); + + let data = message.contract.fail.getData(); + + await token.approve(accounts[1], 10, {from: accounts[2]}); + + try { + await token.transferDataFrom( + accounts[2], message.contract.address, 10, data, + {from: accounts[1]} + ); + assert(false, 'transferDataFrom should have thrown'); + } catch(error) { + assertJump(error); + } + + // transferDataFrom 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 approveData when using token contract address as receiver', async function() { + let data = token.contract.approve.getData(accounts[5], 66); + + try { + await token.approveData(token.contract.address, 100, data, {from: accounts[0]}); + assert(false, 'approveData should have thrown because the spender cannot be the token itself'); + } catch(error) { + assertJump(error); + } + + }); + + it('should fail transferData when using token contract address as receiver', async function() { + + try { + await token.transferData( + token.contract.address, 100, web3.toHex(0), {from: accounts[0]} + ); + assert(false, 'transferData should have thrown because the spender cannot be the token itself'); + } catch(error) { + assertJump(error); + } + + }); + + it('should fail transferDataFrom when using token contract address as receiver', async function() { + + await token.approve(accounts[1], 1, {from: accounts[0]}); + + try { + await token.transferDataFrom( + accounts[0], token.contract.address, 1, web3.toHex(0), {from: accounts[1]} + ); + assert(false, 'transferDataFrom should have thrown because the spender cannot be the token itself'); + } catch(error) { + assertJump(error); + } + + }); + +});