Browse Source

Merge pull request #170 from Recmo/feature/no-owner

Prevent contracts from owning Ether, Tokens and other contracts.
Manuel Aráoz 8 years ago
parent
commit
a344d42a00

+ 18 - 0
contracts/ownership/HasNoContracts.sol

@@ -0,0 +1,18 @@
+pragma solidity ^0.4.8;
+
+import "./Ownable.sol";
+
+/// @title Contracts that should not own Contracts
+/// @author Remco Bloemen <remco@2π.com>
+///
+/// Should contracts (anything Ownable) end up being owned by
+/// this contract, it allows the owner of this contract to
+/// reclaim ownership of the contracts.
+contract HasNoContracts is Ownable {
+
+  /// Reclaim ownership of Ownable contracts
+  function reclaimContract(address contractAddr) external onlyOwner {
+    Ownable contractInst = Ownable(contractAddr);
+    contractInst.transferOwnership(owner);
+  }
+}

+ 42 - 0
contracts/ownership/HasNoEther.sol

@@ -0,0 +1,42 @@
+pragma solidity ^0.4.8;
+
+import "./Ownable.sol";
+
+/// @title Contracts that should not own Ether
+/// @author Remco Bloemen <remco@2π.com>
+///
+/// This tries to block incoming ether to prevent accidental
+/// loss of Ether. Should Ether end up in the contrat, it will
+/// allow the owner to reclaim this ether.
+///
+/// @notice Ether can still be send to this contract by:
+///  * calling functions labeled `payable`
+///  * `selfdestruct(contract_address)`
+///  * mining directly to the contract address
+contract HasNoEther is Ownable {
+
+  /// Constructor that rejects incoming Ether
+  /// @dev The flag `payable` is added so we can access `msg.value`
+  ///      without compiler warning. If we leave out payable, then
+  ///      Solidity will allow inheriting contracts to implement a
+  ///      payable constructor. By doing it this way we prevent a
+  ///      payable constructor from working.
+  ///      Alternatively we could use assembly to access msg.value.
+  function HasNoEther() payable {
+    if(msg.value > 0) {
+      throw;
+    }
+  }
+
+  /// Disallow direct send by settings a default function without `payable`
+  function() external {
+  }
+
+  /// Transfer all Ether owned by the contract to the owner
+  /// @dev What if owner is itself a contract marked HasNoEther?
+  function reclaimEther() external onlyOwner {
+    if(!owner.send(this.balance)) {
+      throw;
+    }
+  }
+}

+ 26 - 0
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 <remco@2π.com>
+///
+/// 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);
+  }
+}

+ 14 - 0
contracts/ownership/NoOwner.sol

@@ -0,0 +1,14 @@
+pragma solidity ^0.4.8;
+
+import "./HasNoEther.sol";
+import "./HasNoTokens.sol";
+import "./HasNoContracts.sol";
+
+/// @title Base contract for contracts that should not own things.
+/// @author Remco Bloemen <remco@2π.com>
+///
+/// Solves a class of errors where a contract accidentally
+/// becomes owner of Ether, Tokens or Owned contracts. See
+/// respective base contracts for details.
+contract NoOwner is HasNoEther, HasNoTokens, HasNoContracts {
+}

+ 35 - 0
test/HasNoContracts.js

@@ -0,0 +1,35 @@
+'use strict';
+import expectThrow from './helpers/expectThrow';
+import toPromise from './helpers/toPromise';
+const Ownable = artifacts.require('../contracts/ownership/Ownable.sol');
+const HasNoContracts = artifacts.require(
+  '../contracts/ownership/HasNoContracts.sol',
+);
+
+contract('HasNoContracts', function(accounts) {
+  let hasNoContracts = null;
+  let ownable = null;
+
+  beforeEach(async () => {
+    // Create contract and token
+    hasNoContracts = await HasNoContracts.new();
+    ownable = await Ownable.new();
+
+    // Force ownership into contract
+    await ownable.transferOwnership(hasNoContracts.address);
+    const owner = await ownable.owner();
+    assert.equal(owner, hasNoContracts.address);
+  });
+
+  it('should allow owner to reclaim contracts', async function() {
+    await hasNoContracts.reclaimContract(ownable.address);
+    const owner = await ownable.owner();
+    assert.equal(owner, accounts[0]);
+  });
+
+  it('should allow only owner to reclaim contracts', async function() {
+    await expectThrow(
+      hasNoContracts.reclaimContract(ownable.address, {from: accounts[1]}),
+    );
+  });
+});

+ 63 - 0
test/HasNoEther.js

@@ -0,0 +1,63 @@
+'use strict';
+import expectThrow from './helpers/expectThrow';
+import toPromise from './helpers/toPromise';
+const HasNoEther = artifacts.require('../contracts/lifecycle/HasNoEther.sol');
+const HasNoEtherTest = artifacts.require('../helpers/HasNoEtherTest.sol');
+const ForceEther = artifacts.require('../helpers/ForceEther.sol');
+
+contract('HasNoEther', function(accounts) {
+  const amount = web3.toWei('1', 'ether');
+
+  it('should be constructorable', async function() {
+    let hasNoEther = await HasNoEtherTest.new();
+  });
+
+  it('should not accept ether in constructor', async function() {
+    await expectThrow(HasNoEtherTest.new({value: amount}));
+  });
+
+  it('should not accept ether', async function() {
+    let hasNoEther = await HasNoEtherTest.new();
+
+    await expectThrow(
+      toPromise(web3.eth.sendTransaction)({
+        from: accounts[1],
+        to: hasNoEther.address,
+        value: amount,
+      }),
+    );
+  });
+
+  it('should allow owner to reclaim ether', async function() {
+    // Create contract
+    let hasNoEther = await HasNoEtherTest.new();
+    const startBalance = await web3.eth.getBalance(hasNoEther.address);
+    assert.equal(startBalance, 0);
+
+    // Force ether into it
+    await ForceEther.new(hasNoEther.address, {value: amount});
+    const forcedBalance = await web3.eth.getBalance(hasNoEther.address);
+    assert.equal(forcedBalance, amount);
+
+    // Reclaim
+    const ownerStartBalance = await web3.eth.getBalance(accounts[0]);
+    await hasNoEther.reclaimEther();
+    const ownerFinalBalance = await web3.eth.getBalance(accounts[0]);
+    const finalBalance = await web3.eth.getBalance(hasNoEther.address);
+    assert.equal(finalBalance, 0);
+    assert.isAbove(ownerFinalBalance, ownerStartBalance);
+  });
+
+  it('should allow only owner to reclaim ether', async function() {
+    // Create contract
+    let hasNoEther = await HasNoEtherTest.new({from: accounts[0]});
+
+    // Force ether into it
+    await ForceEther.new(hasNoEther.address, {value: amount});
+    const forcedBalance = await web3.eth.getBalance(hasNoEther.address);
+    assert.equal(forcedBalance, amount);
+
+    // Reclaim
+    await expectThrow(hasNoEther.reclaimEther({from: accounts[1]}));
+  });
+});

+ 40 - 0
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]}),
+    );
+  });
+});

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

+ 13 - 0
test/helpers/ForceEther.sol

@@ -0,0 +1,13 @@
+pragma solidity ^0.4.8;
+
+// @title Force Ether into a contract.
+// @notice  even
+// if the contract is not payable.
+// @notice To use, construct the contract with the target as argument.
+// @author Remco Bloemen <remco@neufund.org>
+contract ForceEther  {
+  function ForceEther(address target) payable {
+    // Selfdestruct transfers all Ether to the arget address
+    selfdestruct(target);
+  }
+}

+ 11 - 0
test/helpers/HasNoEtherTest.sol

@@ -0,0 +1,11 @@
+pragma solidity ^0.4.8;
+
+import "../../contracts/ownership/HasNoEther.sol";
+
+contract HasNoEtherTest is HasNoEther {
+
+  // Constructor with explicit payable — should still fail
+  function HasNoEtherTest() payable {
+  }
+
+}

+ 4 - 0
test/helpers/toPromise.js

@@ -0,0 +1,4 @@
+export default func =>
+  (...args) =>
+    new Promise((accept, reject) =>
+      func(...args, (error, data) => error ? reject(error) : accept(data)));