From 94d3c447b788c09b6c79cbe8de6caf126af7a593 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 23 Mar 2017 15:22:48 +0000 Subject: [PATCH] Add HasNoTokens --- contracts/ownership/HasNoTokens.sol | 26 +++++++++++++++++++ test/HasNoTokens.js | 40 +++++++++++++++++++++++++++++ test/helpers/ERC23TokenMock.sol | 33 ++++++++++++++++++++++++ test/helpers/expectThrow.js | 10 +++++++- 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 contracts/ownership/HasNoTokens.sol create mode 100644 test/HasNoTokens.js create mode 100644 test/helpers/ERC23TokenMock.sol diff --git a/contracts/ownership/HasNoTokens.sol b/contracts/ownership/HasNoTokens.sol new file mode 100644 index 000000000..9b376930b --- /dev/null +++ b/contracts/ownership/HasNoTokens.sol @@ -0,0 +1,26 @@ +pragma solidity ^0.4.8; + +import "./Ownable.sol"; +import "../token/ERC20Basic.sol"; + +/// @title Contracts that should not own Tokens +/// @author Remco Bloemen +/// +/// This blocks incoming ERC23 tokens to prevent accidental +/// loss of tokens. Should tokens (any ERC20Basic compatible) +/// end up in the contract, it allows the owner to reclaim +/// the tokens. +contract HasNoTokens is Ownable { + + /// Reject all ERC23 compatible tokens + function tokenFallback(address from_, uint value_, bytes data_) external { + throw; + } + + /// Reclaim all ERC20Basic compatible tokens + function reclaimToken(address tokenAddr) external onlyOwner { + ERC20Basic tokenInst = ERC20Basic(tokenAddr); + uint256 balance = tokenInst.balanceOf(this); + tokenInst.transfer(owner, balance); + } +} diff --git a/test/HasNoTokens.js b/test/HasNoTokens.js new file mode 100644 index 000000000..5afe29ef0 --- /dev/null +++ b/test/HasNoTokens.js @@ -0,0 +1,40 @@ +'use strict'; +import expectThrow from './helpers/expectThrow'; +import toPromise from './helpers/toPromise'; +const HasNoTokens = artifacts.require('../contracts/lifecycle/HasNoTokens.sol'); +const ERC23TokenMock = artifacts.require('./helpers/ERC23TokenMock.sol'); + +contract('HasNoTokens', function(accounts) { + let hasNoTokens = null; + let token = null; + + beforeEach(async () => { + // Create contract and token + hasNoTokens = await HasNoTokens.new(); + token = await ERC23TokenMock.new(accounts[0], 100); + + // Force token into contract + await token.transfer(hasNoTokens.address, 10); + const startBalance = await token.balanceOf(hasNoTokens.address); + assert.equal(startBalance, 10); + }); + + it('should not accept ERC23 tokens', async function() { + await expectThrow(token.transferERC23(hasNoTokens.address, 10, '')); + }); + + it('should allow owner to reclaim tokens', async function() { + const ownerStartBalance = await token.balanceOf(accounts[0]); + await hasNoTokens.reclaimToken(token.address); + const ownerFinalBalance = await token.balanceOf(accounts[0]); + const finalBalance = await token.balanceOf(hasNoTokens.address); + assert.equal(finalBalance, 0); + assert.equal(ownerFinalBalance - ownerStartBalance, 10); + }); + + it('should allow only owner to reclaim tokens', async function() { + await expectThrow( + hasNoTokens.reclaimToken(token.address, {from: accounts[1]}), + ); + }); +}); diff --git a/test/helpers/ERC23TokenMock.sol b/test/helpers/ERC23TokenMock.sol new file mode 100644 index 000000000..417c2494b --- /dev/null +++ b/test/helpers/ERC23TokenMock.sol @@ -0,0 +1,33 @@ +pragma solidity ^0.4.8; + + +import '../../contracts/token/BasicToken.sol'; + + +contract ERC23ContractInterface { + function tokenFallback(address _from, uint _value, bytes _data) external; +} + +contract ERC23TokenMock is BasicToken { + + function ERC23TokenMock(address initialAccount, uint initialBalance) { + balances[initialAccount] = initialBalance; + totalSupply = initialBalance; + } + + // ERC23 compatible transfer function (except the name) + function transferERC23(address _to, uint _value, bytes _data) + returns (bool success) + { + transfer(_to, _value); + bool is_contract = false; + assembly { + is_contract := not(iszero(extcodesize(_to))) + } + if(is_contract) { + ERC23ContractInterface receiver = ERC23ContractInterface(_to); + receiver.tokenFallback(msg.sender, _value, _data); + } + return true; + } +} diff --git a/test/helpers/expectThrow.js b/test/helpers/expectThrow.js index 8d0142d4b..45bdcfdb0 100644 --- a/test/helpers/expectThrow.js +++ b/test/helpers/expectThrow.js @@ -5,7 +5,15 @@ export default async promise => { // TODO: Check jump destination to destinguish between a throw // and an actual invalid jump. const invalidJump = error.message.search('invalid JUMP') >= 0; - assert(invalidJump, "Expected throw, got '" + error + "' instead"); + // TODO: When we contract A calls contract B, and B throws, instead + // of an 'invalid jump', we get an 'out of gas' error. How do + // we distinguish this from an actual out of gas event? (The + // testrpc log actually show an 'invalid jump' event.) + const outOfGas = error.message.search('out of gas') >= 0; + assert( + invalidJump || outOfGas, + "Expected throw, got '" + error + "' instead", + ); return; } assert.fail('Expected throw not received');