/* This file is part of cpp-ethereum. cpp-ethereum is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. cpp-ethereum is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with cpp-ethereum. If not, see . */ /** * @author Christian * @date 2015 * Tests for a (comparatively) complex multisig wallet contract. */ #include #include #include #include #include using namespace std; namespace dev { namespace solidity { namespace test { static char const* walletCode = R"DELIMITER( //sol Wallet // Multi-sig, daily-limited account proxy/wallet. // @authors: // Gav Wood // inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a // single, or, crucially, each of a number of, designated owners. // usage: // use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by // some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the // interior is executed. contract multiowned { // struct for the status of a pending operation. struct PendingState { uint yetNeeded; uint ownersDone; uint index; } // this contract only has five types of events: it can accept a confirmation, in which case // we record owner and operation (hash) alongside it. event Confirmation(address owner, bytes32 operation); event Revoke(address owner, bytes32 operation); // some others are in the case of an owner changing. event OwnerChanged(address oldOwner, address newOwner); event OwnerAdded(address newOwner); event OwnerRemoved(address oldOwner); // the last one is emitted if the required signatures change event RequirementChanged(uint newRequirement); // constructor is given number of sigs required to do protected "onlymanyowners" transactions // as well as the selection of addresses capable of confirming them. function multiowned() { m_required = 1; m_numOwners = 1; m_owners[m_numOwners] = uint(msg.sender); m_ownerIndex[uint(msg.sender)] = m_numOwners; } // simple single-sig function modifier. modifier onlyowner { if (isOwner(msg.sender)) _ } // multi-sig function modifier: the operation must have an intrinsic hash in order // that later attempts can be realised as the same underlying operation and // thus count as confirmations. modifier onlymanyowners(bytes32 _operation) { if (confirmed(_operation)) _ } // Revokes a prior confirmation of the given operation function revoke(bytes32 _operation) external { uint ownerIndex = m_ownerIndex[uint(msg.sender)]; // make sure they're an owner if (ownerIndex == 0) return; uint ownerIndexBit = 2**ownerIndex; var pending = m_pending[_operation]; if (pending.ownersDone & ownerIndexBit > 0) { pending.yetNeeded++; pending.ownersDone -= ownerIndexBit; Revoke(msg.sender, _operation); } } function confirmed(bytes32 _operation) internal returns (bool) { // determine what index the present sender is: uint ownerIndex = m_ownerIndex[uint(msg.sender)]; // make sure they're an owner if (ownerIndex == 0) return; var pending = m_pending[_operation]; // if we're not yet working on this operation, switch over and reset the confirmation status. if (pending.yetNeeded == 0) { // reset count of confirmations needed. pending.yetNeeded = m_required; // reset which owners have confirmed (none) - set our bitmap to 0. pending.ownersDone = 0; pending.index = m_pendingIndex.length++; m_pendingIndex[pending.index] = _operation; } // determine the bit to set for this owner. uint ownerIndexBit = 2**ownerIndex; // make sure we (the message sender) haven't confirmed this operation previously. if (pending.ownersDone & ownerIndexBit == 0) { Confirmation(msg.sender, _operation); // ok - check if count is enough to go ahead. if (pending.yetNeeded <= 1) { // enough confirmations: reset and run interior. delete m_pendingIndex[m_pending[_operation].index]; delete m_pending[_operation]; return true; } else { // not enough: record that this owner in particular confirmed. pending.yetNeeded--; pending.ownersDone |= ownerIndexBit; } } } // Replaces an owner `_from` with another `_to`. function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external { if (isOwner(_to)) return; uint ownerIndex = m_ownerIndex[uint(_from)]; if (ownerIndex == 0) return; clearPending(); m_owners[ownerIndex] = uint(_to); m_ownerIndex[uint(_from)] = 0; m_ownerIndex[uint(_to)] = ownerIndex; OwnerChanged(_from, _to); } function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external { if (isOwner(_owner)) return; clearPending(); if (m_numOwners >= c_maxOwners) reorganizeOwners(); if (m_numOwners >= c_maxOwners) return; m_numOwners++; m_owners[m_numOwners] = uint(_owner); m_ownerIndex[uint(_owner)] = m_numOwners; OwnerAdded(_owner); } function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external { uint ownerIndex = m_ownerIndex[uint(_owner)]; if (ownerIndex == 0) return; if (m_required > m_numOwners - 1) return; m_owners[ownerIndex] = 0; m_ownerIndex[uint(_owner)] = 0; clearPending(); reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot OwnerRemoved(_owner); } function reorganizeOwners() private returns (bool) { uint free = 1; while (free < m_numOwners) { while (free < m_numOwners && m_owners[free] != 0) free++; while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--; if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0) { m_owners[free] = m_owners[m_numOwners]; m_ownerIndex[m_owners[free]] = free; m_owners[m_numOwners] = 0; } } } function clearPending() internal { uint length = m_pendingIndex.length; for (uint i = 0; i < length; ++i) if (m_pendingIndex[i] != 0) delete m_pending[m_pendingIndex[i]]; delete m_pendingIndex; } function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external { if (_newRequired > m_numOwners) return; m_required = _newRequired; clearPending(); RequirementChanged(_newRequired); } function isOwner(address _addr) returns (bool) { return m_ownerIndex[uint(_addr)] > 0; } // the number of owners that must confirm the same operation before it is run. uint m_required; // pointer used to find a free slot in m_owners uint m_numOwners; // list of owners uint[256] m_owners; uint constant c_maxOwners = 250; // index on the list of owners to allow reverse lookup mapping(uint => uint) m_ownerIndex; // the ongoing operations. mapping(bytes32 => PendingState) m_pending; bytes32[] m_pendingIndex; } // inheritable "property" contract that enables methods to be protected by placing a linear limit (specifiable) // on a particular resource per calendar day. is multiowned to allow the limit to be altered. resource that method // uses is specified in the modifier. contract daylimit is multiowned { // constructor - just records the present day's index. function daylimit() { m_lastDay = today(); } // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) external { m_dailyLimit = _newLimit; } // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. function resetSpentToday() onlymanyowners(sha3(msg.data)) external { m_spentToday = 0; } // checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and // returns true. otherwise just returns false. function underLimit(uint _value) internal onlyowner returns (bool) { // reset the spend limit if we're on a different day to last time. if (today() > m_lastDay) { m_spentToday = 0; m_lastDay = today(); } // check to see if there's enough left - if so, subtract and return true. if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) { m_spentToday += _value; return true; } return false; } // simple modifier for daily limit. modifier limitedDaily(uint _value) { if (underLimit(_value)) _ } // determines today's index. function today() private constant returns (uint) { return now / 1 days; } uint m_spentToday; uint m_dailyLimit; uint m_lastDay; } // interface contract for multisig proxy contracts; see below for docs. contract multisig { event Deposit(address from, uint value); event SingleTransact(address owner, uint value, address to, bytes data); event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data); event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); function changeOwner(address _from, address _to) external; function execute(address _to, uint _value, bytes _data) external returns (bytes32); function confirm(bytes32 _h) returns (bool); } // usage: // bytes32 h = Wallet(w).from(oneOwner).transact(to, value, data); // Wallet(w).from(anotherOwner).confirm(h); contract Wallet is multisig, multiowned, daylimit { // Transaction structure to remember details of transaction lest it need be saved for a later call. struct Transaction { address to; uint value; bytes data; } // logged events: // Funds has arrived into the wallet (record how much). event Deposit(address from, uint value); // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). event SingleTransact(address owner, uint value, address to, bytes data); // constructor - just pass on the owner arra to the multiowned. event Created(); function Wallet() { Created(); } // kills the contract sending everything to `_to`. function kill(address _to) onlymanyowners(sha3(msg.data)) external { suicide(_to); } // gets called when no other function matches function() { // just being sent some cash? if (msg.value > 0) Deposit(msg.sender, msg.value); } // Outside-visible transact entry point. Executes transacion immediately if below daily spend limit. // If not, goes into multisig process. We provide a hash on return to allow the sender to provide // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value // and _data arguments). They still get the option of using them if they want, anyways. function execute(address _to, uint _value, bytes _data) onlyowner external returns (bytes32 _r) { // first, take the opportunity to check that we're under the daily limit. if (underLimit(_value)) { SingleTransact(msg.sender, _value, _to, _data); // yes - just execute the call. _to.call.value(_value)(_data); return 0; } // determine our operation hash. _r = sha3(msg.data); if (!confirm(_r) && m_txs[_r].to == 0) { m_txs[_r].to = _to; m_txs[_r].value = _value; m_txs[_r].data = _data; ConfirmationNeeded(_r, msg.sender, _value, _to, _data); } } // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order // to determine the body of the transaction from the hash provided. function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) { if (m_txs[_h].to != 0) { m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data); MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data); delete m_txs[_h]; return true; } } function clearPending() internal { uint length = m_pendingIndex.length; for (uint i = 0; i < length; ++i) delete m_txs[m_pendingIndex[i]]; super.clearPending(); } // pending transactions we have at present. mapping (bytes32 => Transaction) m_txs; } )DELIMITER"; static unique_ptr s_compiledWallet; class WalletTestFramework: public ExecutionFramework { protected: void deployWallet(u256 const& _value = 0) { if (!s_compiledWallet) { m_optimize = true; m_compiler.reset(false, m_addStandardSources); m_compiler.addSource("", walletCode); ETH_TEST_REQUIRE_NO_THROW(m_compiler.compile(m_optimize, m_optimizeRuns), "Compiling contract failed"); s_compiledWallet.reset(new bytes(m_compiler.getBytecode("Wallet"))); } sendMessage(*s_compiledWallet, true, _value); BOOST_REQUIRE(!m_output.empty()); } }; /// This is a test suite that tests optimised code! BOOST_FIXTURE_TEST_SUITE(SolidityWallet, WalletTestFramework) BOOST_AUTO_TEST_CASE(creation) { deployWallet(200); BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(m_sender, h256::AlignRight)) == encodeArgs(true)); BOOST_REQUIRE(callContractFunction("isOwner(address)", ~h256(m_sender, h256::AlignRight)) == encodeArgs(false)); } BOOST_AUTO_TEST_CASE(add_owners) { deployWallet(200); Address originalOwner = m_sender; BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x12)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x12)) == encodeArgs(true)); // now let the new owner add someone m_sender = Address(0x12); BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x13)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x13)) == encodeArgs(true)); // and check that a non-owner cannot add a new owner m_sender = Address(0x50); BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x20)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x20)) == encodeArgs(false)); // finally check that all the owners are there BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(originalOwner, h256::AlignRight)) == encodeArgs(true)); BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x12)) == encodeArgs(true)); BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x13)) == encodeArgs(true)); } BOOST_AUTO_TEST_CASE(change_owners) { deployWallet(200); BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x12)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x12)) == encodeArgs(true)); BOOST_REQUIRE(callContractFunction("changeOwner(address,address)", h256(0x12), h256(0x13)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x12)) == encodeArgs(false)); BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x13)) == encodeArgs(true)); } BOOST_AUTO_TEST_CASE(remove_owner) { deployWallet(200); // add 10 owners for (unsigned i = 0; i < 10; ++i) { BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x12 + i)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x12 + i)) == encodeArgs(true)); } // check they are there again for (unsigned i = 0; i < 10; ++i) BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x12 + i)) == encodeArgs(true)); // remove the odd owners for (unsigned i = 0; i < 10; ++i) if (i % 2 == 1) BOOST_REQUIRE(callContractFunction("removeOwner(address)", h256(0x12 + i)) == encodeArgs()); // check the result for (unsigned i = 0; i < 10; ++i) BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x12 + i)) == encodeArgs(i % 2 == 0)); // add them again for (unsigned i = 0; i < 10; ++i) if (i % 2 == 1) BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x12 + i)) == encodeArgs()); // check everyone is there for (unsigned i = 0; i < 10; ++i) BOOST_REQUIRE(callContractFunction("isOwner(address)", h256(0x12 + i)) == encodeArgs(true)); } BOOST_AUTO_TEST_CASE(multisig_value_transfer) { deployWallet(200); BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x12)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x13)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x14)) == encodeArgs()); // 4 owners, set required to 3 BOOST_REQUIRE(callContractFunction("changeRequirement(uint256)", u256(3)) == encodeArgs()); // check that balance is and stays zero at destination address h256 opHash("f916231db11c12e0142dc51f23632bc655de87c63f83fc928c443e90f7aa364a"); BOOST_CHECK_EQUAL(m_state.balance(Address(0x05)), 0); m_sender = Address(0x12); BOOST_REQUIRE(callContractFunction("execute(address,uint256,bytes)", h256(0x05), 100, 0x60, 0x00) == encodeArgs(opHash)); BOOST_CHECK_EQUAL(m_state.balance(Address(0x05)), 0); m_sender = Address(0x13); BOOST_REQUIRE(callContractFunction("execute(address,uint256,bytes)", h256(0x05), 100, 0x60, 0x00) == encodeArgs(opHash)); BOOST_CHECK_EQUAL(m_state.balance(Address(0x05)), 0); m_sender = Address(0x14); BOOST_REQUIRE(callContractFunction("execute(address,uint256,bytes)", h256(0x05), 100, 0x60, 0x00) == encodeArgs(opHash)); // now it should go through BOOST_CHECK_EQUAL(m_state.balance(Address(0x05)), 100); } BOOST_AUTO_TEST_CASE(daylimit) { deployWallet(200); BOOST_REQUIRE(callContractFunction("setDailyLimit(uint256)", h256(100)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x12)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x13)) == encodeArgs()); BOOST_REQUIRE(callContractFunction("addOwner(address)", h256(0x14)) == encodeArgs()); // 4 owners, set required to 3 BOOST_REQUIRE(callContractFunction("changeRequirement(uint256)", u256(3)) == encodeArgs()); // try to send tx over daylimit BOOST_CHECK_EQUAL(m_state.balance(Address(0x05)), 0); m_sender = Address(0x12); BOOST_REQUIRE( callContractFunction("execute(address,uint256,bytes)", h256(0x05), 150, 0x60, 0x00) != encodeArgs(u256(0)) ); BOOST_CHECK_EQUAL(m_state.balance(Address(0x05)), 0); // try to send tx under daylimit by stranger m_sender = Address(0x77); BOOST_REQUIRE( callContractFunction("execute(address,uint256,bytes)", h256(0x05), 90, 0x60, 0x00) == encodeArgs(u256(0)) ); BOOST_CHECK_EQUAL(m_state.balance(Address(0x05)), 0); // now send below limit by owner m_sender = Address(0x12); BOOST_REQUIRE( callContractFunction("execute(address,uint256,bytes)", h256(0x05), 90, 0x60, 0x00) == encodeArgs(u256(0)) ); BOOST_CHECK_EQUAL(m_state.balance(Address(0x05)), 90); } //@todo test data calls BOOST_AUTO_TEST_SUITE_END() } } } // end namespaces