├── .editorconfig ├── .gitignore ├── .solcover.js ├── .soliumignore ├── .soliumrc.json ├── .travis.yml ├── .yarnrc ├── CHANGELOG.md ├── README.md ├── build └── doc │ └── .gitignore ├── contracts ├── Bidirectional.sol ├── Conditional.sol ├── DistributeEth.sol ├── DistributeToken.sol ├── LibBidirectional.sol ├── LibCommon.sol ├── LibLineup.sol ├── LibMultisig.sol ├── Lineup.sol ├── Migrations.sol ├── Multisig.sol ├── Proxy.sol ├── PublicRegistry.sol ├── TestContract.sol ├── TestDelegatecall.sol ├── TestToken.sol └── TransferToken.sol ├── doc ├── Contract.md ├── channel-states.svg └── development │ └── verification.md ├── migrations ├── 10_deploy_LibLineup.ts ├── 1_initial_migration.ts ├── 3_deploy_LibBidirectional.ts ├── 4_deploy_registry.ts ├── 5_deploy_transfer_token.ts ├── 6_deploy_multisigLibrary.ts ├── 7_deploy_conditionalCallLibrary.ts ├── 8_deploy_publicRegistryLibrary.ts └── 9_deploy_proxy.ts ├── package.json ├── src ├── MerkleTree.ts ├── Units.ts ├── globals.d.ts └── index.ts ├── support ├── natspec │ ├── NatSpec.ts │ ├── compiler.ts │ ├── index.ts │ └── sources.ts ├── tsconfig.json └── wrap │ ├── Context.ts │ ├── ContractTemplate.ts │ ├── IndexTemplate.ts │ ├── helpers.ts │ ├── index.ts │ └── templates │ ├── _event.mustache │ ├── _function.mustache │ ├── _getter.mustache │ ├── _method_input.mustache │ ├── _method_output.mustache │ ├── _params.mustache │ ├── contract.mustache │ └── index.mustache ├── test ├── scenarios │ ├── Cooperative.test.ts │ └── Dispute.test.ts ├── support │ └── index.ts └── unit │ ├── Bidirectional.test.ts │ ├── Conditional.test.ts │ ├── Lineup.test.ts │ ├── Multisig._test.ts │ ├── Multisig.test.ts │ └── PublicRegistry.test.ts ├── truffle.js ├── tsconfig.json ├── tslint.json ├── types └── ethereumjs-abi │ └── index.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.sol] 14 | indent_size = 4 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | node_modules/ 4 | .env 5 | yarn-error.log 6 | 7 | **/*.js.map 8 | **/*.d.ts 9 | **/*.js 10 | coverage.json 11 | coverage/ 12 | build/wrappers 13 | 14 | .DS_Store 15 | dist/ 16 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | copyPackages: ['zeppelin-solidity'] 3 | } 4 | -------------------------------------------------------------------------------- /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | contracts/zeppelin-solidity 3 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:all", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "quotes": [ 8 | "error", 9 | "double" 10 | ], 11 | "indentation": [ 12 | "error", 13 | 4 14 | ], 15 | "arg-overflow": [ 16 | "warning", 17 | 5 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - '10' 5 | before_install: 6 | - curl -o- -L https://yarnpkg.com/install.sh | bash 7 | - export PATH="$HOME/.yarn/bin:$PATH" 8 | script: 9 | - yarn lint 10 | - yarn test 11 | after_script: 12 | - yarn coverage && cat coverage/lcov.info | coveralls 13 | notifications: 14 | slack: 15 | secure: ROidOwDK3i9EJY9x2OojBmVGQYkxxZi92w1AsWfKutDktCaqocXLpjWLuPedTRs3/VFl/1YDMPhYzWQBXzVzlmMJ3kC4EKvZbBAu3C/3M+bDjo/hvqBeDLANn06WVmzKkcCHmhh6YJLK5a7C4ygoy9gS0SAzMC4Ou9ZJ8+V4umBf66//WRwNWZ+MKJ2P2qkuy4M55o3Ke98ozgukPTSuHUdpW4Zwo57C/YiJAy2ZMvda4oKYRmuRu7q0PkilHO+WRGv09CsAwTBCEUJluLva9zJuS1S+x+015LIzrya/3OqLp0ySoDOYD4PnR37H60/Vcvd1SGXXAj13lSZt7ByOVYKxfyQMmTe7CGFbpwgiZXVFUmQTygQeiPH0XZVzM8jxyaCRnUHeyGHunhDhsiDXSQ34Q6db29oH1h4KHMZOoJR0FetVontetZoDi7y6UzFiAvsfLc2CYkg7yukUFcIClmjTUSXt/+yjsLAgq9QviNo9HIvR/OtaoKQkom080jXn//Ntj3JEBbqLg1QIbKjmjpp5LwLRdd1CSoNahIiS07IOQV3kK89av22LX2VD5Ooqggq9VkTevN1T2yT3RoW6k6K9IwjVyQtjLdWbYFg6EbQwptvjl+eRa6D5I1OgkntN5MXGnS3aps3kistKvbv/xvpo8dgaw9bo9BQVBmxEF7Q= 16 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --install.pure-lockfile true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.2] - 2017-11-1 2 | ### Added 3 | 4 | - Add README.md 5 | 6 | ## [2.0.1] - 2017-11-1 7 | ### Fixed 8 | 9 | - Add 0x prefix to sign 10 | 11 | ## [2.0.0] - 2017-11-1 12 | ### Changed 13 | 14 | - All monetary values are now in BigNumber -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Machinomy contracts [![Build Status][travis-img]][travis] [![Coverage Status][coveralls-img]][coveralls] 2 | [travis]: https://travis-ci.org/machinomy/machinomy-contracts 3 | [travis-img]: https://img.shields.io/travis/machinomy/machinomy-contracts.svg 4 | [coveralls]: https://coveralls.io/github/machinomy/machinomy-contracts?branch=master 5 | [coveralls-img]: https://coveralls.io/repos/github/machinomy/machinomy-contracts/badge.svg?branch=master 6 | 7 | Machinomy contracts is a TypeScript interface for Ethereum contracts managed by [Truffle](https://github.com/trufflesuite/truffle) used by [Machinomy](https://github.com/machinomy/machinomy). 8 | 9 | ## Install 10 | ``` 11 | $ yarn add @machinomy/contracts 12 | ``` 13 | 14 | ## Workflow 15 | Use [testrpc](https://github.com/ethereumjs/testrpc) for fast development. Start testrpc by command: 16 | ``` 17 | $ testrpc 18 | ``` 19 | 20 | Then deploy contracts to the tesrpc network: 21 | ``` 22 | $ yarn truffle:migrate 23 | ``` 24 | 25 | Truffle generates json files by default. You need to compile the json files to ts files. Run: 26 | ``` 27 | $ yarn build 28 | ``` 29 | Now package is ready to use by Machinony. 30 | 31 | ## Deployment 32 | To deploy the package to the Ropsten network you need to run local geth instance and then run commands: 33 | ``` 34 | $ yarn truffle:migrate --network ropsten 35 | $ yarn build 36 | ``` 37 | 38 | ## Testing 39 | 40 | ``` 41 | $ yarn prepublish && yarn test 42 | ``` 43 | 44 | To log gas cost for transactions: 45 | ``` 46 | $ LOG_GAS_COST=true yarn test 47 | ``` 48 | -------------------------------------------------------------------------------- /build/doc/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machinomy/mc2/9113e7c20383f0b7e47313244696023900e2d69f/build/doc/.gitignore -------------------------------------------------------------------------------- /contracts/Bidirectional.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "./Multisig.sol"; 4 | import "./LibBidirectional.sol"; 5 | 6 | 7 | /// @title Bidirectional Ether payment channels contract. 8 | contract Bidirectional { 9 | LibBidirectional.State public state; 10 | 11 | function Bidirectional(address _multisig, uint32 _settlementPeriod) public payable { 12 | state.multisig = Multisig(_multisig); 13 | state.lastUpdate = block.number; 14 | state.settlementPeriod = _settlementPeriod; 15 | // DidOpen 16 | } 17 | 18 | function () payable public {} 19 | 20 | function update( 21 | uint256 _nonce, 22 | uint256 _toSender, 23 | uint256 _toReceiver, 24 | bytes _senderSig, 25 | bytes _receiverSig 26 | ) external 27 | { 28 | LibBidirectional.update( 29 | state, 30 | _nonce, 31 | _toSender, 32 | _toReceiver, 33 | _senderSig, 34 | _receiverSig 35 | ); 36 | // DidUpdate 37 | } 38 | 39 | function close(uint256 _toSender, uint256 _toReceiver, bytes _senderSig, bytes _receiverSig) public { 40 | require(LibBidirectional.canClose(state, _toSender, _toReceiver, _senderSig, _receiverSig)); 41 | var (sender, receiver,) = state.multisig.state(); 42 | receiver.transfer(_toReceiver); 43 | sender.transfer(_toSender); 44 | selfdestruct(state.multisig); 45 | } 46 | 47 | function withdraw() public { 48 | require(!LibBidirectional.isSettling(state)); 49 | 50 | var (sender, receiver,) = state.multisig.state(); 51 | receiver.transfer(state.toReceiver); 52 | sender.transfer(state.toSender); 53 | selfdestruct(state.multisig); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/Conditional.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "./Lineup.sol"; 4 | import "./PublicRegistry.sol"; 5 | 6 | 7 | contract Conditional { 8 | function doCall( 9 | address _registry, 10 | bytes32 _lineupCF, 11 | bytes _proof, 12 | address _destination, 13 | uint256 _value, 14 | bytes _data 15 | ) public 16 | { 17 | 18 | PublicRegistry registry = PublicRegistry(_registry); 19 | address lineupAddress = registry.resolve(_lineupCF); 20 | Lineup lineup = Lineup(lineupAddress); 21 | 22 | bytes32 hash = callHash(_destination, _value, _data); 23 | require(lineup.isContained(_proof, hash)); 24 | require(_destination.call.value(_value)(_data)); // solium-disable-line security/no-call-value 25 | } 26 | 27 | function doDelegate( 28 | address _registry, 29 | bytes32 _lineupCF, 30 | bytes _proof, 31 | address _destination, 32 | uint256 _value, 33 | bytes _data 34 | ) public 35 | { 36 | 37 | PublicRegistry registry = PublicRegistry(_registry); 38 | address lineupAddress = registry.resolve(_lineupCF); 39 | Lineup lineup = Lineup(lineupAddress); 40 | 41 | bytes32 hash = callHash(_destination, _value, _data); 42 | require(lineup.isContained(_proof, hash)); 43 | require(_destination.delegatecall(_data)); // solium-disable-line security/no-low-level-calls 44 | } 45 | 46 | function callHash(address _destination, uint256 _value, bytes _data) public pure returns (bytes32) { 47 | return keccak256(_destination, _value, _data); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /contracts/DistributeEth.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | 4 | contract DistributeEth { 5 | function execute(address a, address b, uint256 amountA, uint256 amountB) external { 6 | a.transfer(amountA); 7 | b.transfer(amountB); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/DistributeToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; 4 | 5 | 6 | contract DistributeToken { 7 | function execute(address _token, address a, address b, uint256 amountA, uint256 amountB) public { 8 | StandardToken token = StandardToken(_token); 9 | require(token.transfer(a, amountA)); 10 | require(token.transfer(b, amountB)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/LibBidirectional.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "./Multisig.sol"; 4 | import "./LibCommon.sol"; 5 | 6 | 7 | library LibBidirectional { 8 | struct State { 9 | Multisig multisig; 10 | uint256 lastUpdate; 11 | uint256 settlementPeriod; 12 | uint256 nonce; 13 | uint256 toSender; 14 | uint256 toReceiver; 15 | } 16 | 17 | function canUpdate( 18 | State storage _self, 19 | uint256 _nonce, 20 | uint256 _toSender, 21 | uint256 _toReceiver, 22 | bytes _senderSig, 23 | bytes _receiverSig 24 | ) public view returns (bool) 25 | { 26 | bool isNonceHigher = _nonce > _self.nonce; 27 | // FIXME Add a secret to prevent replay attach 28 | bytes32 hash = LibCommon.recoveryDigest(keccak256(_nonce, _toSender, _toReceiver)); 29 | bool isSigned = _self.multisig.isUnanimous(hash, _senderSig, _receiverSig); 30 | return isSettling(_self) && isNonceHigher && isSigned; 31 | } 32 | 33 | function update( 34 | State storage _state, 35 | uint256 _nonce, 36 | uint256 _toSender, 37 | uint256 _toReceiver, 38 | bytes _senderSig, 39 | bytes _receiverSig 40 | ) public 41 | { 42 | var can = canUpdate( 43 | _state, 44 | _nonce, 45 | _toSender, 46 | _toReceiver, 47 | _senderSig, 48 | _receiverSig 49 | ); 50 | require(can); 51 | _state.toSender = _toSender; 52 | _state.toReceiver = _toReceiver; 53 | _state.nonce = _nonce; 54 | _state.lastUpdate = block.number; 55 | } 56 | 57 | function canClose( 58 | State storage _self, 59 | uint256 _toSender, 60 | uint256 _toReceiver, 61 | bytes _senderSig, 62 | bytes _receiverSig 63 | ) public view returns (bool) 64 | { 65 | // FIXME Add a secret to prevent replay attach 66 | bytes32 hash = LibCommon.recoveryDigest(keccak256("c", _toSender, _toReceiver)); 67 | bool isSigned = _self.multisig.isUnanimous(hash, _senderSig, _receiverSig); 68 | return isSettling(_self) && isSigned; 69 | } 70 | 71 | function isSettling(State storage _state) public view returns(bool) { 72 | return block.number <= _state.lastUpdate + _state.settlementPeriod; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /contracts/LibCommon.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | 4 | library LibCommon { 5 | 6 | // TODO Move it to the separate library 7 | struct ShareStateState { 8 | address owner; 9 | uint256 nonce; 10 | bytes32 merkleRoot; 11 | uint256 updatePeriod; 12 | uint256 lastUpdate; 13 | } 14 | 15 | function recoveryDigest(bytes32 hash) public pure returns(bytes32) { 16 | bytes memory prefix = "\x19Ethereum Signed Message:\n32"; 17 | return keccak256(prefix, hash); 18 | } 19 | 20 | function executeHashCalc(address thisAddress, address destination, uint256 value, bytes data, uint256 nonce) public pure returns(bytes32) { 21 | return recoveryDigest(keccak256(thisAddress, destination, value, data, nonce)); 22 | } 23 | 24 | function executeHashCheck(bytes32 hash, bytes senderSig, bytes receiverSig, address sender, address receiver) public pure returns(bool) { 25 | return sender == recover(hash, senderSig) && receiver == recover(hash, receiverSig); 26 | } 27 | 28 | /** 29 | * @dev [code from zeppelin-solidity] Recover signer address from a message by using his signature 30 | * @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address. 31 | * @param sig bytes signature, the signature is generated using web3.eth.sign() 32 | */ 33 | function recover(bytes32 hash, bytes sig) public pure returns (address) { 34 | bytes32 r; 35 | bytes32 s; 36 | uint8 v; 37 | 38 | //Check the signature length 39 | if (sig.length != 65) { 40 | return (address(0)); 41 | } 42 | 43 | // Divide the signature in r, s and v variables 44 | assembly { // solium-disable-line security/no-inline-assembly 45 | r := mload(add(sig, 32)) 46 | s := mload(add(sig, 64)) 47 | v := byte(0, mload(add(sig, 96))) 48 | } 49 | 50 | // Version of signature should be 27 or 28, but 0 and 1 are also possible versions 51 | if (v < 27) { 52 | v += 27; 53 | } 54 | 55 | // If the version is correct return the signer address 56 | if (v != 27 && v != 28) { 57 | return (address(0)); 58 | } else { 59 | return ecrecover(hash, v, r, s); 60 | } 61 | } 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /contracts/LibLineup.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "./Multisig.sol"; 4 | import "./LibCommon.sol"; 5 | 6 | 7 | library LibLineup { 8 | struct State { 9 | uint256 nonce; 10 | bytes32 merkleRoot; 11 | uint256 updatePeriod; 12 | uint256 lastUpdate; 13 | Multisig multisig; 14 | } 15 | 16 | event Trace(bool a); 17 | function update(State storage _self, uint256 _nonce, bytes32 _merkleRoot, bytes _senderSig, bytes _receiverSig) internal { 18 | var hash = keccak256(_merkleRoot, _nonce); 19 | require(_self.multisig.isUnanimous(LibCommon.recoveryDigest(hash), _senderSig, _receiverSig)); 20 | require(_nonce > _self.nonce); 21 | require(block.number <= _self.lastUpdate + _self.updatePeriod); 22 | 23 | _self.merkleRoot = _merkleRoot; 24 | _self.nonce = _nonce; 25 | _self.lastUpdate = block.number; 26 | } 27 | 28 | function isContained(State storage _self, bytes _proof, bytes32 _hashlock) public view returns (bool) { 29 | bytes32 proofElement; 30 | bytes32 cursor = _hashlock; 31 | bool result = false; 32 | 33 | if (block.number >= _self.lastUpdate + _self.updatePeriod) { 34 | for (uint256 i = 32; i <= _proof.length; i += 32) { 35 | assembly { proofElement := mload(add(_proof, i)) } // solium-disable-line security/no-inline-assembly 36 | 37 | if (cursor < proofElement) { 38 | cursor = keccak256(cursor, proofElement); 39 | } else { 40 | cursor = keccak256(proofElement, cursor); 41 | } 42 | } 43 | result = cursor == _self.merkleRoot; 44 | } 45 | return result; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /contracts/LibMultisig.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "zeppelin-solidity/contracts/math/SafeMath.sol"; 4 | import "./LibCommon.sol"; 5 | 6 | 7 | library LibMultisig { 8 | using SafeMath for uint256; 9 | 10 | struct State { 11 | address sender; 12 | address receiver; 13 | uint256 nonce; 14 | } 15 | 16 | function callDigest( 17 | address _self, 18 | address _destination, 19 | uint256 _value, 20 | bytes _data, 21 | uint256 _nonce 22 | ) public pure returns(bytes32) 23 | { 24 | return LibCommon.recoveryDigest(keccak256(_self, _destination, _value, _data, _nonce)); 25 | } 26 | 27 | function delegateDigest( 28 | address _self, 29 | address _destination, 30 | bytes _data, 31 | uint256 _nonce 32 | ) public pure returns(bytes32) 33 | { 34 | return LibCommon.recoveryDigest(keccak256(_self, _destination, _data, _nonce)); 35 | } 36 | 37 | function isUnanimous( 38 | State storage state, 39 | bytes32 hash, 40 | bytes senderSig, 41 | bytes receiverSig 42 | ) public view returns(bool) 43 | { 44 | return state.sender == LibCommon.recover(hash, senderSig) && 45 | state.receiver == LibCommon.recover(hash, receiverSig); 46 | } 47 | 48 | function doCall( 49 | address self, 50 | address destination, 51 | uint256 value, 52 | bytes data, 53 | bytes senderSig, 54 | bytes receiverSig, 55 | State storage state 56 | ) public 57 | { 58 | require(isUnanimous(state, callDigest(self, destination, value, data, state.nonce), senderSig, receiverSig)); 59 | state.nonce = state.nonce.add(1); 60 | } 61 | 62 | function doDelegatecall( 63 | address self, 64 | address destination, 65 | bytes data, 66 | bytes senderSig, 67 | bytes receiverSig, 68 | State storage state 69 | ) public 70 | { 71 | require(isUnanimous(state, delegateDigest(self, destination, data, state.nonce), senderSig, receiverSig)); 72 | state.nonce = state.nonce.add(1); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /contracts/Lineup.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "./LibLineup.sol"; 4 | import "./Multisig.sol"; 5 | 6 | 7 | // Optimisation Idea: Use shared Lineup. 8 | contract Lineup { 9 | LibLineup.State public state; 10 | 11 | event Trace(bytes32 a); 12 | function Lineup(bytes32 _merkleRoot, uint256 _updatePeriod, address _multisig) public { 13 | state.merkleRoot = _merkleRoot; 14 | state.updatePeriod = _updatePeriod; 15 | state.lastUpdate = block.number; 16 | state.multisig = Multisig(_multisig); 17 | } 18 | 19 | function update(uint256 _nonce, bytes32 _merkleRoot, bytes _senderSig, bytes _receiverSig) external { 20 | LibLineup.update(state, _nonce, _merkleRoot, _senderSig, _receiverSig); 21 | } 22 | 23 | function isContained(bytes proof, bytes32 hashlock) public view returns (bool) { 24 | return LibLineup.isContained(state, proof, hashlock); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | if (msg.sender == owner) 10 | _; 11 | } 12 | 13 | function Migrations() public { 14 | owner = msg.sender; 15 | } 16 | 17 | function setCompleted(uint completed) public restricted { 18 | last_completed_migration = completed; 19 | } 20 | 21 | function upgrade(address newAddress) public restricted { 22 | Migrations upgraded = Migrations(newAddress); 23 | upgraded.setCompleted(last_completed_migration); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/Multisig.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "./LibMultisig.sol"; 4 | 5 | 6 | contract Multisig { 7 | LibMultisig.State public state; 8 | 9 | function Multisig(address _sender, address _receiver) public { 10 | state.sender = _sender; 11 | state.receiver = _receiver; 12 | } 13 | 14 | function () payable external {} 15 | 16 | function doCall( 17 | address destination, 18 | uint256 value, 19 | bytes data, 20 | bytes senderSig, 21 | bytes receiverSig 22 | ) external 23 | { 24 | LibMultisig.doCall( 25 | address(this), 26 | destination, 27 | value, 28 | data, 29 | senderSig, 30 | receiverSig, 31 | state 32 | ); 33 | require(destination.call.value(value)(data)); // solium-disable-line security/no-call-value 34 | } 35 | 36 | function doDelegate( 37 | address destination, 38 | bytes data, 39 | bytes senderSig, 40 | bytes receiverSig 41 | ) external 42 | { 43 | LibMultisig.doDelegatecall( 44 | address(this), 45 | destination, 46 | data, 47 | senderSig, 48 | receiverSig, 49 | state 50 | ); 51 | require(destination.delegatecall(data)); // solium-disable-line security/no-low-level-calls 52 | } 53 | 54 | function isUnanimous(bytes32 _hash, bytes _senderSig, bytes _receiverSig) public view returns(bool) { 55 | return LibMultisig.isUnanimous(state, _hash, _senderSig, _receiverSig); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /contracts/Proxy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "./PublicRegistry.sol"; 4 | 5 | 6 | // @title Proxy call via counterfactual address. 7 | // @dev This may really suck. TODO Check output types 8 | contract Proxy { 9 | function doCall(address _registry, bytes32 _destination, uint256 value, bytes data) public { 10 | address destination = findDestination(_registry, _destination); 11 | require(destination.call.value(value)(data)); // solium-disable-line security/no-call-value 12 | } 13 | 14 | function doDelegateCall(address _registry, bytes32 _destination, bytes data) public { 15 | address destination = findDestination(_registry, _destination); 16 | require(destination.delegatecall(data)); // solium-disable-line security/no-low-level-calls 17 | } 18 | 19 | function findDestination(address _registry, bytes32 _destination) internal view returns (address destination) { 20 | PublicRegistry registry = PublicRegistry(_registry); 21 | destination = registry.resolve(_destination); 22 | require(destination != address(0x0)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/PublicRegistry.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | 4 | contract PublicRegistry { 5 | mapping (bytes32 => address) _contracts; 6 | 7 | event DidDeploy(bytes32 indexed id, address indexed deployed, address indexed owner); 8 | 9 | function deploy(bytes _code, bytes32 nonce) public { 10 | address realAddress; 11 | // solium-disable-next-line security/no-inline-assembly 12 | assembly { 13 | realAddress := create(0, add(_code, 0x20), mload(_code)) 14 | } 15 | bytes32 cfAddress = counterfactualAddress(_code, nonce); 16 | _contracts[cfAddress] = realAddress; 17 | 18 | DidDeploy(cfAddress, realAddress, msg.sender); 19 | } 20 | 21 | function resolve(bytes32 cfAddress) public view returns (address) { 22 | return _contracts[cfAddress]; 23 | } 24 | 25 | function counterfactualAddress(bytes _code, bytes32 nonce) public pure returns (bytes32) { 26 | return keccak256(_code, nonce); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/TestContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; 4 | 5 | 6 | contract TestContract { 7 | uint256 public nonce; 8 | 9 | event DidUpdate(uint256 nonce); 10 | 11 | function TestContract(uint256 _nonce) public { 12 | nonce = _nonce; 13 | } 14 | 15 | function () payable public {} 16 | 17 | function updateNonce(uint256 _nonce) payable public { 18 | nonce = _nonce; 19 | DidUpdate(nonce); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/TestDelegatecall.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | 4 | contract TestDelegatecall { 5 | event Executed(); 6 | function execute(uint256 amountA, uint256 amountB) public { 7 | require(amountA > amountB); 8 | Executed(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/TestToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; 4 | import "zeppelin-solidity/contracts/token/ERC20/MintableToken.sol"; 5 | 6 | 7 | contract TestToken is StandardToken, MintableToken { 8 | function TestToken() public { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/TransferToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "./PublicRegistry.sol"; 4 | import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; 5 | 6 | 7 | // @title Proxy call via counterfactual address. 8 | contract TransferToken { 9 | function execute(address _registry, address _token, bytes32 _destination, uint256 amount) public { 10 | PublicRegistry registry = PublicRegistry(_registry); 11 | address destination = registry.resolve(_destination); 12 | require(destination != address(0x0)); 13 | 14 | StandardToken token = StandardToken(_token); 15 | require(token.transfer(destination, amount)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /doc/Contract.md: -------------------------------------------------------------------------------- 1 | # How the contract is supposed to work 2 | 3 | Broker contract manages uni-directional payment channels. A payment channel lifecycle is depicted below: 4 | 5 | ![Channel States](https://cdn.rawgit.com/machinomy/machinomy-contracts/683e17f1/doc/channel-states.svg) 6 | 7 | **No Channel → Open:** 8 | 9 | Initiator, that is a sender, deposits money to the contract, calls `createChannel` method of the contract. 10 | 11 | **Open:** 12 | 13 | While the channel is open, the sender and the receiver (the parties), are free to exchange with IOU notes. Validity of IOU note is checked in `canClaim` contract method. 14 | 15 | **Open → Settled:** 16 | 17 | Settlement process that is initiated by the receiver. It provides IOU note to `settle` method. That triggers distribution of the deposited money to the parties, according to presented IOU note. 18 | 19 | **Open → Settling → Settled:** 20 | 21 | Settlement process that is initiated by the sender. It is driven by an assumption of an unresponsive receiver. The sender starts the settlement process by calling `startSettle` method, provides the expected payout to the receiver. If the latter does not respond indeed, the sender could finish the process by calling `finishSettle`. There is no constraint the payout amount provided by the sender. It could be zero. To mitigate that, `createChannel` accepts `settlementPeriod` param that specifies how long the channel waits for the receiver responds. The receiver is free not to accept payment and provide service, if she finds that period inadequate. 22 | 23 | **Settled → Closed:** 24 | 25 | A _settled_ channel is not _closed_. It could probably have money attached to it. Also, closed channel no longer occupies space in the list of payment channels. 26 | 27 | Either sender, or receiver, or _contract owner_ is free to call `close` to finally close the channel. That involves moving remaining money to the sender, and removing the channel from the list maintained by the contract. 28 | -------------------------------------------------------------------------------- /doc/channel-states.svg: -------------------------------------------------------------------------------- 1 | Asset 1Sender creates a channelOpenSettledSettlingSender starts the settlingReceiver finishes the settlingAnyone can close the settled channelInitiated by ReceiverClosedNo Channel -------------------------------------------------------------------------------- /doc/development/verification.md: -------------------------------------------------------------------------------- 1 | # Contract verification 2 | 3 | To display contract source code on etherscan you need to verify it: 4 | 1. [ropsten](https://ropsten.etherscan.io/verifyContract) 5 | 2. [kovan](https://kovan.etherscan.io/verifyContract) 6 | 7 | Use [truffle-flattener](https://github.com/alcuadrado/truffle-flattener) to get flattened solidity contract code. 8 | 9 | Code example for constructor arguments encoding into ABI: 10 | 11 | ```js 12 | var abi = require('ethereumjs-abi') 13 | var parameterTypes = ["uint32"]; 14 | var parameterValues = [42] 15 | var encoded = abi.rawEncode(parameterTypes, parameterValues); 16 | console.log(encoded.toString('hex')); 17 | ``` 18 | -------------------------------------------------------------------------------- /migrations/10_deploy_LibLineup.ts: -------------------------------------------------------------------------------- 1 | import * as Deployer from 'truffle-deployer' 2 | 3 | const LibLineup = artifacts.require('LibLineup') 4 | const LibMultisig = artifacts.require('LibMultisig') 5 | const LibCommon = artifacts.require('LibCommon') 6 | 7 | module.exports = function (deployer: Deployer) { 8 | LibLineup.link(LibMultisig) 9 | LibLineup.link(LibCommon) 10 | return deployer.deploy(LibLineup) 11 | } 12 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.ts: -------------------------------------------------------------------------------- 1 | import * as Deployer from 'truffle-deployer' 2 | 3 | const Migrations = artifacts.require('./Migrations.sol') 4 | 5 | module.exports = function (deployer: Deployer) { 6 | return deployer.deploy(Migrations) 7 | } 8 | -------------------------------------------------------------------------------- /migrations/3_deploy_LibBidirectional.ts: -------------------------------------------------------------------------------- 1 | import * as Deployer from 'truffle-deployer' 2 | 3 | const LibCommon = artifacts.require('LibCommon') 4 | const LibBidirectional = artifacts.require('LibBidirectional.sol') 5 | 6 | module.exports = function (deployer: Deployer) { 7 | return deployer.deploy(LibCommon).then(() => { 8 | return deployer.link(LibCommon, LibBidirectional) 9 | }).then(() => { 10 | return deployer.deploy(LibBidirectional) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /migrations/4_deploy_registry.ts: -------------------------------------------------------------------------------- 1 | import * as Deployer from 'truffle-deployer' 2 | 3 | const PublicRegistry = artifacts.require('PublicRegistry') 4 | 5 | module.exports = function (deployer: Deployer) { 6 | return deployer.deploy(PublicRegistry) 7 | } 8 | -------------------------------------------------------------------------------- /migrations/5_deploy_transfer_token.ts: -------------------------------------------------------------------------------- 1 | import * as Deployer from 'truffle-deployer' 2 | 3 | const TransferToken = artifacts.require('TransferToken') 4 | 5 | module.exports = function (deployer: Deployer) { 6 | return deployer.deploy(TransferToken) 7 | } 8 | -------------------------------------------------------------------------------- /migrations/6_deploy_multisigLibrary.ts: -------------------------------------------------------------------------------- 1 | import * as Deployer from 'truffle-deployer' 2 | 3 | const ECRecovery = artifacts.require('ECRecovery.sol') 4 | const LibMultisig = artifacts.require('LibMultisig.sol') 5 | const LibCommon = artifacts.require('LibCommon.sol') 6 | 7 | module.exports = function (deployer: Deployer) { 8 | return deployer.deploy(ECRecovery).then(() => { 9 | return deployer.link(ECRecovery, LibMultisig) 10 | }).then(() => { 11 | return deployer.deploy(LibCommon) 12 | }).then(() => { 13 | return deployer.link(LibCommon, LibMultisig) 14 | }).then(() => { 15 | return deployer.deploy(LibMultisig) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /migrations/7_deploy_conditionalCallLibrary.ts: -------------------------------------------------------------------------------- 1 | import * as Deployer from 'truffle-deployer' 2 | 3 | const Conditional = artifacts.require('Conditional') 4 | 5 | module.exports = function (deployer: Deployer) { 6 | return deployer.deploy(Conditional) 7 | } 8 | -------------------------------------------------------------------------------- /migrations/8_deploy_publicRegistryLibrary.ts: -------------------------------------------------------------------------------- 1 | import * as Deployer from 'truffle-deployer' 2 | 3 | const PublicRegistry = artifacts.require('PublicRegistry') 4 | const PublicRegistryLibrary = artifacts.require('PublicRegistryLibrary') 5 | 6 | module.exports = function (deployer: Deployer) { 7 | return deployer.deploy(PublicRegistryLibrary).then(() => { 8 | return deployer.link(PublicRegistryLibrary, PublicRegistry) 9 | }).then(() => { 10 | return deployer.deploy(PublicRegistry) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /migrations/9_deploy_proxy.ts: -------------------------------------------------------------------------------- 1 | import * as Deployer from 'truffle-deployer' 2 | 3 | const Proxy = artifacts.require('Proxy') 4 | 5 | module.exports = function (deployer: Deployer) { 6 | return deployer.deploy(Proxy) 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@machinomy/contracts", 3 | "version": "4.0.8", 4 | "description": "Machinomy contracts managed by Truffle", 5 | "license": "AGPL-3.0-or-later", 6 | "homepage": "https://github.com/machinomy/machinomy-contracts#readme", 7 | "bugs": { 8 | "url": "https://github.com/machinomy/machinomy-contracts/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/machinomy/machinomy-contracts.git" 13 | }, 14 | "main": "dist/src/index.js", 15 | "types": "dist/src/index.d.ts", 16 | "files": [ 17 | "dist/" 18 | ], 19 | "directories": { 20 | "test": "test" 21 | }, 22 | "config": { 23 | "truffleTests": "test/**/*.test.js", 24 | "wrappedArtifacts": "build/contracts/@(Test*|Bidirectional|Conditional|DistributeEth|DistributeToken|PublicRegistry|Lineup|TestToken|Multisig|TransferToken|TestContract|Proxy|ECRecovery).json" 25 | }, 26 | "natspec": { 27 | "whitelist": [ 28 | "Unidirectional" 29 | ] 30 | }, 31 | "scripts": { 32 | "prepublish": "yarn lint && yarn build --outDir dist/ && cp -r build/contracts dist/build", 33 | "lint": "yarn lint:solidity && yarn lint:typescript", 34 | "lint:solidity": "solium -d contracts/", 35 | "lint:typescript": "tslint --format stylish --project .", 36 | "build": "yarn support:wrap && tsc --project tsconfig.json", 37 | "support:wrap": "yarn truffle:compile && yarn support:build && node support/wrap $npm_package_config_wrappedArtifacts --output $npm_package_config_wrappers_dir", 38 | "support:build": "tsc --project support/tsconfig.json", 39 | "truffle:test": "run-with-testrpc 'truffle test $npm_package_config_truffleTests'", 40 | "truffle:compile": "truffle compile", 41 | "truffle:migrate": "truffle migrate", 42 | "test": "yarn build && yarn truffle:test", 43 | "doc:natspec": "node support/natspec", 44 | "coverage": "./node_modules/.bin/solidity-coverage" 45 | }, 46 | "author": "Sergey Ukustov ", 49 | "David Wolever " 50 | ], 51 | "devDependencies": { 52 | "@machinomy/types-ethereumjs-units": "^0.0.2", 53 | "@machinomy/types-truffle": "^0.0.2", 54 | "@machinomy/types-truffle-artifactor": "^0.0.1", 55 | "@machinomy/types-truffle-compile": "^0.0.2", 56 | "@machinomy/types-truffle-config": "^0.0.2", 57 | "@machinomy/types-truffle-contract-sources": "^0.0.1", 58 | "@machinomy/types-truffle-resolver": "^0.0.1", 59 | "@machinomy/types-web3": "^0.0.7", 60 | "@types/bignumber.js": "4.0.2", 61 | "@types/chai": "^4.0.5", 62 | "@types/chai-as-promised": "^7.1.0", 63 | "@types/mkdirp": "^0.5.2", 64 | "@types/node": "^8.0.28", 65 | "@types/yargs": "^10.0.1", 66 | "chai": "^4.1.2", 67 | "chai-as-promised": "^7.1.1", 68 | "coveralls": "^3.0.0", 69 | "ethereumjs-abi": "https://github.com/ethereumjs/ethereumjs-abi", 70 | "ethereumjs-units": "^0.2.0", 71 | "handlebars": "^4.0.11", 72 | "mkdirp": "^0.5.1", 73 | "run-with-testrpc": "^0.2.1", 74 | "solidity-coverage": "^0.4.9", 75 | "solium": "^1.0.11", 76 | "truffle": "^4.1.0", 77 | "truffle-flattener": "^1.2.3", 78 | "tslint": "^5.7.0", 79 | "tslint-config-standard": "^6.0.1", 80 | "types-ethereumjs-util": "https://github.com/machinomy/types-ethereumjs-util", 81 | "typescript": "^2.8.3", 82 | "yargs": "^10.1.1", 83 | "zeppelin-solidity": "^1.6.0" 84 | }, 85 | "dependencies": { 86 | "bignumber.js": "4.1.0", 87 | "buffer": "^5.0.7", 88 | "ethereumjs-util": "^5.1.5", 89 | "keccak": "^1.4.0", 90 | "truffle-contract": "^3.0.0", 91 | "utf8": "^3.0.0", 92 | "web3": "^0.20.1" 93 | }, 94 | "resolutions": { 95 | "upath": "^1.0.5", 96 | "sha3": "^1.2.1" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/MerkleTree.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import * as util from 'ethereumjs-util' 3 | 4 | function isHash (buffer: Buffer): boolean { 5 | return buffer.length === 32 && Buffer.isBuffer(buffer) 6 | } 7 | 8 | function isNotHash (buffer: Buffer): boolean { 9 | return !isHash(buffer) 10 | } 11 | 12 | function combinedHash (first: Buffer, second: Buffer): Buffer { 13 | if (!second) { 14 | return first 15 | } 16 | if (!first) { 17 | return second 18 | } 19 | let sorted = Buffer.concat([first, second].sort(Buffer.compare)) 20 | return util.sha3(sorted) 21 | } 22 | 23 | function getNextLayer (elements: Array): Array { 24 | return elements.reduce>((layer, element, index, arr) => { 25 | if (index % 2 === 0) { 26 | layer.push(combinedHash(element, arr[index + 1])) 27 | } 28 | return layer 29 | }, []) 30 | } 31 | 32 | function getLayers (elements: Array): Array> { 33 | if (elements.length === 0) { 34 | return [[Buffer.from('')]] 35 | } 36 | let layers = [] 37 | layers.push(elements) 38 | while (layers[layers.length - 1].length > 1) { 39 | layers.push(getNextLayer(layers[layers.length - 1])) 40 | } 41 | return layers 42 | } 43 | 44 | function getPair (index: number, layer: Array): Buffer|null { 45 | let pairIndex = index % 2 ? index - 1 : index + 1 46 | if (pairIndex < layer.length) { 47 | return layer[pairIndex] 48 | } else { 49 | return null 50 | } 51 | } 52 | 53 | function deduplicate (buffers: Array): Array { 54 | return buffers.filter((buffer, i) => { 55 | return buffers.findIndex(e => e.equals(buffer)) === i 56 | }) 57 | } 58 | 59 | export default class MerkleTree { 60 | elements: Array 61 | layers: Array> 62 | _root?: Buffer 63 | 64 | get root (): Buffer { 65 | if (!this._root) { 66 | this._root = this.layers[this.layers.length - 1][0] 67 | } 68 | return this._root 69 | } 70 | 71 | constructor (elements: Array) { 72 | // check buffers 73 | if (elements.some(isNotHash)) { 74 | throw new Error('elements must be 32 byte buffers') 75 | } 76 | 77 | this.elements = deduplicate(elements) 78 | this.elements.sort(Buffer.compare) 79 | 80 | this.layers = getLayers(this.elements) 81 | } 82 | 83 | verify (proof: Array, element: Buffer): boolean { 84 | return this.root.equals(proof.reduce((hash, pair) => combinedHash(hash, pair), element)) 85 | } 86 | 87 | proof (element: Buffer): Array { 88 | let index = this.elements.findIndex(e => e.equals(element)) 89 | if (index === -1) { 90 | throw new Error('element not found in merkle tree') 91 | } 92 | 93 | return this.layers.reduce((proof, layer) => { 94 | let pair = getPair(index, layer) 95 | if (pair) { 96 | proof.push(pair) 97 | } 98 | index = Math.floor(index / 2) 99 | return proof 100 | }, []) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Units.ts: -------------------------------------------------------------------------------- 1 | import * as ethUnits from 'ethereumjs-units' 2 | import * as BigNumber from 'bignumber.js' 3 | 4 | namespace Units { 5 | export function convert (value: number | BigNumber.BigNumber, fromUnit: string, toUnit: string): BigNumber.BigNumber { 6 | let stringNumber = ethUnits.convert(value.toString(), fromUnit, toUnit) 7 | return new BigNumber.BigNumber(stringNumber) 8 | } 9 | } 10 | 11 | export default Units 12 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | // Truffle injects the following into the global scope 2 | declare var before: any 3 | declare var beforeEach: any 4 | declare var describe: any 5 | declare var xdescribe: any 6 | declare var it: any 7 | declare var specify: any 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import PublicRegistry from '../build/wrappers/PublicRegistry' 2 | import Multisig from '../build/wrappers/Multisig' 3 | import Proxy from '../build/wrappers/Proxy' 4 | import ECRecovery from '../build/wrappers/ECRecovery' 5 | import TransferToken from '../build/wrappers/TransferToken' 6 | import DistributeEth from '../build/wrappers/DistributeEth' 7 | import DistributeToken from '../build/wrappers/DistributeToken' 8 | import Lineup from '../build/wrappers/Lineup' 9 | import Conditional from '../build/wrappers/Conditional' 10 | import Bidirectional from '../build/wrappers/Bidirectional' 11 | import TestContract from '../build/wrappers/TestContract' 12 | import TestToken from '../build/wrappers/TestToken' 13 | import * as ethUtil from 'ethereumjs-util' 14 | 15 | export { 16 | PublicRegistry, 17 | Multisig, 18 | Proxy, 19 | ECRecovery, 20 | TransferToken, 21 | DistributeEth, 22 | DistributeToken, 23 | Conditional, 24 | Lineup, 25 | Bidirectional, 26 | TestContract, 27 | TestToken 28 | } 29 | 30 | export function randomId (digits: number = 3) { 31 | const datePart = new Date().getTime() * Math.pow(10, digits) 32 | const extraPart = Math.floor(Math.random() * Math.pow(10, digits)) // 3 random digits 33 | return datePart + extraPart // 16 digits 34 | } 35 | 36 | export function channelId (sender: string, receiver: string): string { 37 | let random = randomId() 38 | let buffer = ethUtil.sha3(sender + receiver + random) 39 | return ethUtil.bufferToHex(buffer) 40 | } 41 | -------------------------------------------------------------------------------- /support/natspec/NatSpec.ts: -------------------------------------------------------------------------------- 1 | import * as Web3 from 'web3' 2 | import * as util from 'ethereumjs-util' 3 | import { CompiledResult, ContractEntry } from './compiler' 4 | 5 | enum AbiType { 6 | Function = 'function', 7 | Constructor = 'constructor', 8 | Event = 'event', 9 | Fallback = 'fallback' 10 | } 11 | 12 | export namespace NatSpec { 13 | export type Container = {[name: string]: Entry} 14 | 15 | export interface Entry { 16 | author?: string 17 | title?: string 18 | fileName: string 19 | name?: string 20 | abi?: Web3.ContractAbi 21 | abiDocs: any 22 | } 23 | 24 | function functionSignature (signature: string): string { 25 | return util.sha3(signature).toString('hex').substr(0, 8) 26 | } 27 | 28 | function functionDefinition (entry: ContractEntry, definition: Web3.MethodAbi): any { 29 | let inputParams = definition.inputs 30 | let name = definition.name 31 | let signature = `${name}(${inputParams.map(i => i.type).join(',')})` 32 | let devDocs = entry.devdoc.methods[signature] || {} 33 | let userDocs = entry.userdoc.methods[signature] || {} 34 | 35 | let params = devDocs.params || {} 36 | let inputs = inputParams.map(param => ({ ...param, description: params[param.name] })) 37 | delete devDocs.params 38 | 39 | let outputParams = {} 40 | let outputs = [] 41 | try { 42 | outputParams = JSON.parse(devDocs.return) 43 | } catch (e) { 44 | try { 45 | const split = devDocs.return.split(' ') 46 | const name = split.shift() 47 | outputParams = { [name]: split.join(' ') } 48 | } catch (e2) { /* */ } 49 | } 50 | try { 51 | outputs = definition.outputs.map(param => ({ ...param, description: outputParams[param.name] })) 52 | } catch (e) { /* */ } 53 | 54 | return { 55 | ...definition, 56 | ...devDocs, 57 | ...userDocs, 58 | inputs, 59 | outputs, 60 | signature, 61 | signatureHash: signature && functionSignature(signature) 62 | } 63 | } 64 | 65 | function eventDefinition (entry: ContractEntry, definition: Web3.EventAbi): any { 66 | let inputParams = definition.inputs 67 | let name = definition.name 68 | let signature = `${name}(${inputParams.map(i => i.type).join(',')})` 69 | let devDocs = entry.devdoc.methods[signature] || {} 70 | let userDocs = entry.userdoc.methods[signature] || {} 71 | 72 | let params = devDocs.params || {} 73 | let inputs = inputParams.map(param => ({ ...param, description: params[param.name] })) 74 | delete devDocs.params 75 | 76 | return { 77 | ...definition, 78 | ...devDocs, 79 | ...userDocs, 80 | inputs, 81 | signature, 82 | signatureHash: signature && functionSignature(signature) 83 | } 84 | } 85 | 86 | 87 | function buildDefinition (entry: ContractEntry, definition: Web3.AbiDefinition): any { 88 | switch (definition.type) { 89 | case AbiType.Function: 90 | return functionDefinition(entry, definition) 91 | case AbiType.Event: 92 | return eventDefinition(entry, definition) 93 | case AbiType.Constructor: 94 | return { 95 | ...definition, 96 | inputs: definition.inputs 97 | } 98 | case AbiType.Fallback: 99 | return { 100 | ...definition, 101 | inputs: [] 102 | } 103 | default: 104 | throw new Error(`Got unexpected definition ${definition.type}`) 105 | } 106 | } 107 | 108 | function buildEntry (fileName: string, name: string, entry: ContractEntry): Entry { 109 | return { 110 | fileName: fileName, 111 | author: entry.devdoc.author, 112 | title: entry.devdoc.title, 113 | name: name, 114 | abiDocs: entry.abi.map(definition => buildDefinition(entry, definition)), 115 | abi: entry.abi 116 | } 117 | } 118 | 119 | export async function build (input: CompiledResult, whitelist: Array = []): Promise { 120 | let contracts = input.contracts 121 | let result: Container = {} 122 | Object.keys(contracts).forEach(fileName => { 123 | let contractEntry = contracts[fileName] 124 | let name = Object.keys(contractEntry)[0] 125 | if (whitelist.length > 0 && whitelist.includes(name)) { 126 | result[name] = buildEntry(fileName, name, contractEntry[name]) 127 | } 128 | }) 129 | 130 | return result 131 | } 132 | } 133 | 134 | export default NatSpec 135 | -------------------------------------------------------------------------------- /support/natspec/compiler.ts: -------------------------------------------------------------------------------- 1 | import * as solc from 'solc' 2 | import * as Web3 from 'web3' 3 | import { FullText } from './sources' 4 | 5 | export interface MethodDevDoc { 6 | details?: string 7 | params?: {[method: string]: string} 8 | return?: any 9 | } 10 | 11 | export interface DevDoc { 12 | author?: string 13 | title?: string 14 | name?: string 15 | methods?: {[signature: string]: MethodDevDoc} 16 | } 17 | 18 | export interface AbiDocDefinition { 19 | 20 | } 21 | 22 | export interface UserDoc { 23 | methods?: {[signature: string]: any} 24 | } 25 | 26 | export interface ContractEntry { 27 | devdoc?: DevDoc 28 | userdoc?: UserDoc 29 | abi?: Web3.ContractAbi 30 | abiDocs?: AbiDocDefinition[] 31 | } 32 | 33 | export interface CompiledResult { 34 | contracts: { 35 | [fileName: string]: { 36 | [name: string]: ContractEntry 37 | } 38 | } 39 | } 40 | 41 | export async function doc (fullText: FullText): Promise { 42 | const standardInput = { 43 | language: 'Solidity', 44 | sources: {}, 45 | settings: { 46 | outputSelection: { 47 | '*': { 48 | '*': [ 49 | 'devdoc', 50 | 'userdoc', 51 | 'abi' 52 | ] 53 | } 54 | } 55 | } 56 | } 57 | for (let name in fullText) { 58 | standardInput.sources[name] = { 59 | content: fullText[name] 60 | } 61 | } 62 | let result = solc.compileStandard(JSON.stringify(standardInput)) 63 | return JSON.parse(result)as CompiledResult 64 | } 65 | -------------------------------------------------------------------------------- /support/natspec/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as fs from 'fs' 4 | import * as path from 'path' 5 | import * as sources from './sources' 6 | import * as compiler from './compiler' 7 | import NatSpec from './NatSpec' 8 | 9 | async function write (fileName: string, data: string): Promise { 10 | return new Promise((resolve, reject) => { 11 | fs.writeFile(fileName, data, error => { 12 | error ? reject(error) : resolve() 13 | }) 14 | }) 15 | } 16 | 17 | async function main () { 18 | let packageJsonFilename = path.resolve(__dirname, '../../package.json') 19 | let packageJson = JSON.parse(fs.readFileSync(packageJsonFilename).toString()) 20 | let whitelist = packageJson.natspec.whitelist 21 | let config = await sources.currentConfig() 22 | let fullText = await sources.requiredSources(config) 23 | let solcOutput = await compiler.doc(fullText) 24 | let natSpec = await NatSpec.build(solcOutput, whitelist) 25 | for (let name in natSpec) { 26 | let documentation = natSpec[name] 27 | let outputFile = path.join(config.build_directory, 'doc', `${name}.json`) 28 | let asString = JSON.stringify(documentation, null, 4) 29 | await write(outputFile, asString) 30 | } 31 | } 32 | 33 | main().catch(err => { 34 | console.error(err) 35 | process.exit(1) 36 | }) 37 | -------------------------------------------------------------------------------- /support/natspec/sources.ts: -------------------------------------------------------------------------------- 1 | import * as truffleContractSources from 'truffle-contract-sources' 2 | import * as Profiler from 'truffle-compile/profiler' 3 | import * as Config from 'truffle-config' 4 | import * as Resolver from 'truffle-resolver' 5 | import * as Artifactor from 'truffle-artifactor' 6 | 7 | export type FullText = {[name: string]: string} 8 | 9 | /** 10 | * Configuration of the current Truffle project. 11 | */ 12 | export async function currentConfig (): Promise { 13 | let config = Config.default() 14 | config.resolver = new Resolver(config) 15 | config.artifactor = new Artifactor() 16 | config.paths = await contractPaths(config) 17 | config.base_path = config.contracts_directory 18 | return config 19 | } 20 | 21 | /** 22 | * Subset of configuration needed to [[contractPaths]] function. 23 | */ 24 | interface ContractFilesConfig { 25 | contracts_directory: string 26 | } 27 | 28 | /** 29 | * Paths to the contracts managed by Truffle. 30 | */ 31 | export async function contractPaths (config: ContractFilesConfig): Promise> { 32 | return new Promise>((resolve, reject) => { 33 | truffleContractSources(config.contracts_directory, (err, files) => { 34 | err ? reject(err) : resolve(files) 35 | }) 36 | }) 37 | } 38 | 39 | /** 40 | * Subset of configuration needed to [[requiredSources]] function. 41 | */ 42 | interface RequiredSourcesConfig { 43 | base_path: string 44 | resolver: any 45 | paths: Array 46 | } 47 | 48 | /** 49 | * Sources of contracts along with the dependencies, for the current Truffle project. 50 | */ 51 | export async function requiredSources (config: RequiredSourcesConfig): Promise { 52 | return new Promise((resolve, reject) => { 53 | Profiler.required_sources(config, (err, sources) => { 54 | err ? reject(err) : resolve(sources) 55 | }) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /support/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": [ "es2017" ], 6 | "typeRoots": [ 7 | "../node_modules/@types", 8 | "../node_modules/@machinomy" 9 | ] 10 | }, 11 | "include": [ 12 | "../node_modules/**/*/index.d.ts", 13 | "../types/**/index.d.ts", 14 | "./**/*.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /support/wrap/Context.ts: -------------------------------------------------------------------------------- 1 | import * as Web3 from 'web3' 2 | 3 | export interface MethodAbi extends Web3.MethodAbi { 4 | singleReturnValue: boolean 5 | } 6 | 7 | export default interface Context { 8 | artifact: string 9 | contractName: string 10 | relativeArtifactPath: string 11 | getters: Array 12 | functions: Array 13 | events: Array 14 | } 15 | -------------------------------------------------------------------------------- /support/wrap/ContractTemplate.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | import * as Web3 from 'web3' 4 | import * as Handlebars from 'handlebars' 5 | import Context, { MethodAbi } from './Context' 6 | import * as helpers from './helpers' 7 | 8 | const ABI_TYPE_FUNCTION = 'function' 9 | const ABI_TYPE_EVENT = 'event' 10 | 11 | function isAbiFunction (abi: Web3.AbiDefinition): abi is Web3.MethodAbi { 12 | return abi.type === ABI_TYPE_FUNCTION 13 | } 14 | 15 | function isAbiEvent (abi: Web3.AbiDefinition): abi is Web3.EventAbi { 16 | return abi.type === ABI_TYPE_EVENT 17 | } 18 | 19 | export default class ContractTemplate { 20 | handlebars: typeof Handlebars 21 | templatesDir: string 22 | outputDir: string 23 | private _template?: Handlebars 24 | 25 | constructor (templatesDir: string, outputDir: string) { 26 | this.handlebars = Handlebars.create() 27 | this.templatesDir = templatesDir 28 | this.outputDir = outputDir 29 | this.registerPartials() 30 | this.registerHelpers() 31 | } 32 | 33 | get template () { 34 | if (this._template) { 35 | return this._template 36 | } else { 37 | let contents = this.readTemplate('contract.mustache') 38 | this._template = this.handlebars.compile(contents) 39 | return this._template 40 | } 41 | } 42 | 43 | registerPartials () { 44 | fs.readdirSync(this.templatesDir).forEach(file => { 45 | let match = file.match(/^_(\w+)\.(handlebars|mustache)/) 46 | if (match) { 47 | this.handlebars.registerPartial(match[1], this.readTemplate(file)) 48 | } 49 | }) 50 | } 51 | 52 | registerHelpers () { 53 | this.handlebars.registerHelper('inputType', helpers.inputType) 54 | this.handlebars.registerHelper('outputType', helpers.outputType) 55 | } 56 | 57 | render (abiFilePath: string) { 58 | let artifact = JSON.parse(fs.readFileSync(abiFilePath).toString()) 59 | let abi = artifact.abi 60 | if (abi) { 61 | let methods = abi.filter(isAbiFunction).map((abi: MethodAbi) => { 62 | if (abi.outputs.length === 1) { 63 | abi.singleReturnValue = true 64 | } 65 | abi.inputs = abi.inputs.map(input => { 66 | input.name = input.name ? input.name : 'index' 67 | return input 68 | }) 69 | return abi 70 | }) 71 | let getters = methods.filter((abi: MethodAbi) => abi.constant) 72 | let functions = methods.filter((abi: MethodAbi) => !abi.constant) 73 | 74 | let events = abi.filter(isAbiEvent) 75 | 76 | let contractName = path.parse(abiFilePath).name 77 | const basename = path.basename(abiFilePath, path.extname(abiFilePath)) 78 | const filePath = `${this.outputDir}/${basename}.ts` 79 | const relativeArtifactPath = path.relative(this.outputDir, abiFilePath) 80 | 81 | let context: Context = { 82 | artifact: JSON.stringify(artifact, null, 2), 83 | contractName: contractName, 84 | relativeArtifactPath: relativeArtifactPath, 85 | getters: getters, 86 | functions: functions, 87 | events: events 88 | } 89 | let code = this.template(context) 90 | fs.writeFileSync(filePath, code) 91 | } else { 92 | throw new Error(`No ABI found in ${abiFilePath}.`) 93 | } 94 | } 95 | 96 | protected readTemplate (name: string) { 97 | let file = path.resolve(this.templatesDir, name) 98 | return fs.readFileSync(file).toString() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /support/wrap/IndexTemplate.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | import * as Handlebars from 'handlebars' 4 | import * as helpers from './helpers' 5 | 6 | export interface Wrap { 7 | name: string, 8 | path: string 9 | } 10 | 11 | export interface Context { 12 | wrappers: Array 13 | } 14 | 15 | export default class IndexTemplate { 16 | handlebars: typeof Handlebars 17 | templatesDir: string 18 | outputDir: string 19 | private _template?: Handlebars 20 | 21 | constructor (templatesDir: string, outputDir: string) { 22 | this.handlebars = Handlebars.create() 23 | this.templatesDir = templatesDir 24 | this.outputDir = outputDir 25 | this.registerPartials() 26 | this.registerHelpers() 27 | } 28 | 29 | get template () { 30 | if (this._template) { 31 | return this._template 32 | } else { 33 | let contents = this.readTemplate('index.mustache') 34 | this._template = this.handlebars.compile(contents) 35 | return this._template 36 | } 37 | } 38 | 39 | registerPartials () { 40 | fs.readdirSync(this.templatesDir).forEach(file => { 41 | let match = file.match(/^_(\w+)\.(handlebars|mustache)/) 42 | if (match) { 43 | this.handlebars.registerPartial(match[1], this.readTemplate(file)) 44 | } 45 | }) 46 | } 47 | 48 | registerHelpers () { 49 | this.handlebars.registerHelper('inputType', helpers.inputType) 50 | this.handlebars.registerHelper('outputType', helpers.outputType) 51 | } 52 | 53 | render (filePaths: Array) { 54 | let wrappers = [] 55 | filePaths.forEach(filePath => { 56 | const basename = path.basename(filePath, path.extname(filePath)) 57 | wrappers.push({ 58 | name: path.parse(filePath).name, 59 | path: `./${basename}` 60 | }) 61 | }) 62 | let context = { wrappers } 63 | let code = this.template(context) 64 | let indexFilePath = `${this.outputDir}/index.ts` 65 | fs.writeFileSync(indexFilePath, code) 66 | } 67 | 68 | protected readTemplate (name: string) { 69 | let file = path.resolve(this.templatesDir, name) 70 | return fs.readFileSync(file).toString() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /support/wrap/helpers.ts: -------------------------------------------------------------------------------- 1 | type Mapping = { 2 | regex: string 3 | tsType: string 4 | } 5 | 6 | const TYPE_MAPPING = [ 7 | {regex: '^string$', tsType: 'string'}, 8 | {regex: '^address$', tsType: 'string'}, 9 | {regex: '^bool$', tsType: 'boolean'}, 10 | {regex: '^u?int\\d*$', tsType: 'BigNumber.BigNumber'}, 11 | {regex: '^bytes\\d*$', tsType: 'string'} 12 | ] 13 | 14 | const INPUT_TYPE_MAPPING = [ 15 | {regex: '^u?int(8|16|32)?$', tsType: 'number|BigNumber.BigNumber'} 16 | ].concat(TYPE_MAPPING) 17 | 18 | const ARRAY_BRACES = /\[\d*]$/ 19 | 20 | function isArray (solidityType: string): boolean { 21 | return !!solidityType.match(ARRAY_BRACES) 22 | } 23 | 24 | function typeConversion (types: Array, solidityType: string): string { 25 | if (isArray(solidityType)) { 26 | const solidityItemType = solidityType.replace(ARRAY_BRACES, '') 27 | const type = typeConversion(types, solidityItemType) 28 | return `${type}[]` 29 | } else { 30 | let mapping = types.find(mapping => !!solidityType.match(mapping.regex)) 31 | if (mapping) { 32 | return mapping.tsType 33 | } else { 34 | throw new Error(`Unknown Solidity type found: ${solidityType}`) 35 | } 36 | } 37 | } 38 | 39 | export function inputType (solidityType: string): string { 40 | return typeConversion(INPUT_TYPE_MAPPING, solidityType) 41 | } 42 | 43 | export function outputType (solidityType: string): string { 44 | return typeConversion(TYPE_MAPPING, solidityType) 45 | } 46 | -------------------------------------------------------------------------------- /support/wrap/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as yargs from 'yargs' 4 | import * as mkdirp from 'mkdirp' 5 | import * as path from 'path' 6 | import * as glob from 'glob' 7 | import * as fs from 'fs' 8 | import ContractTemplate from './ContractTemplate' 9 | import IndexTemplate from './IndexTemplate' 10 | 11 | let args = yargs 12 | .option('output', { 13 | describe: 'Folder for generated files', 14 | alias: 'o' 15 | }) 16 | .argv 17 | 18 | let pattern = args._[0] 19 | let fileNames = glob.sync(pattern) 20 | if (fileNames.length) { 21 | let templatesDir = path.resolve(__dirname, 'templates') 22 | let outputDir = path.resolve(__dirname, '..', '..', 'build', 'wrappers') 23 | let transformer = new ContractTemplate(templatesDir, outputDir) 24 | let indexTransformer = new IndexTemplate(templatesDir, outputDir) 25 | 26 | if (!fs.existsSync(outputDir)) { 27 | mkdirp.sync(outputDir) 28 | } 29 | 30 | fileNames.forEach(fileName => { 31 | transformer.render(fileName) 32 | }) 33 | 34 | indexTransformer.render(fileNames) 35 | } else { 36 | console.error(`No Truffle Contract artifact found at ${pattern}`) 37 | process.exit(1) 38 | } 39 | -------------------------------------------------------------------------------- /support/wrap/templates/_event.mustache: -------------------------------------------------------------------------------- 1 | export interface {{this.name}} { 2 | {{#if inputs}} 3 | {{#each inputs}} 4 | {{name}}: {{#inputType type}}{{/inputType}} 5 | {{/each}} 6 | {{/if}} 7 | } 8 | 9 | export function is{{this.name}}Event (something: truffle.AnyTransactionEvent): something is truffle.TransactionEvent<{{this.name}}> { 10 | return something.event === '{{this.name}}' 11 | } 12 | -------------------------------------------------------------------------------- /support/wrap/templates/_function.mustache: -------------------------------------------------------------------------------- 1 | {{this.name}}: { 2 | ({{> method_input inputs=inputs trailingComma=true}}options?: Web3.CallData): Promise 3 | call ({{> method_input inputs=inputs trailingComma=true}}options?: Web3.CallData): Promise<{{> method_output outputs=outputs}}> 4 | estimateGas ({{> method_input inputs=inputs}}): Promise 5 | request ({{> method_input inputs=inputs}}): { method: string, params: [{to: string, data: string}]} 6 | } 7 | -------------------------------------------------------------------------------- /support/wrap/templates/_getter.mustache: -------------------------------------------------------------------------------- 1 | {{this.name}}: { 2 | ({{> method_input inputs=inputs}}): Promise<{{> method_output outputs=outputs}}> 3 | call ({{> method_input inputs=inputs}}): Promise<{{> method_output outputs=outputs}}> 4 | } 5 | -------------------------------------------------------------------------------- /support/wrap/templates/_method_input.mustache: -------------------------------------------------------------------------------- 1 | {{~#if inputs~}} 2 | {{~#each inputs~}}{{name}}: {{#inputType type}} {{/inputType}}{{#unless @last}}, {{/unless}}{{~/each~}} 3 | {{~#if trailingComma~}}, {{/if}} 4 | {{~/if~}} 5 | -------------------------------------------------------------------------------- /support/wrap/templates/_method_output.mustache: -------------------------------------------------------------------------------- 1 | {{~#if outputs~}} 2 | {{~#singleReturnValue~}} 3 | {{#outputType outputs.0.type}}{{/outputType}} 4 | {{~/singleReturnValue~}} 5 | {{~^singleReturnValue~}} 6 | [{{#each outputs}}{{#outputType type}}{{/outputType}}{{#unless @last}}, {{/unless}}{{/each}}] 7 | {{~/singleReturnValue~}} 8 | {{~else~}} 9 | void 10 | {{~/if~}} 11 | -------------------------------------------------------------------------------- /support/wrap/templates/_params.mustache: -------------------------------------------------------------------------------- 1 | {{#each inputs}} 2 | {{name}}, 3 | {{/each}} 4 | -------------------------------------------------------------------------------- /support/wrap/templates/contract.mustache: -------------------------------------------------------------------------------- 1 | import * as BigNumber from 'bignumber.js' 2 | import * as Web3 from 'web3' 3 | import * as truffle from 'truffle-contract' 4 | 5 | export namespace {{contractName}} { 6 | export const ARTIFACT = require('{{relativeArtifactPath}}') 7 | 8 | export interface Contract { 9 | address: string 10 | 11 | constructor: { 12 | web3: Web3 13 | } 14 | 15 | {{#each getters}} 16 | {{> getter contractName=../contractName}} 17 | {{/each}} 18 | 19 | {{#each functions}} 20 | {{> function contractName=../contractName}} 21 | {{/each}} 22 | 23 | send: (value: BigNumber.BigNumber | number) => Promise 24 | sendTransaction: (opts: Web3.CallData) => Promise 25 | } 26 | 27 | {{#each events}} 28 | {{> event contractName=../contractName}} 29 | {{/each}} 30 | 31 | export function contract (provider?: Web3.Provider, defaults?: Web3.CallData): truffle.TruffleContract { 32 | let instance = truffle(ARTIFACT) 33 | if (provider) { 34 | instance.setProvider(provider) 35 | } 36 | if (defaults) { 37 | instance.defaults(defaults) 38 | } 39 | return instance 40 | } 41 | } 42 | 43 | export default {{contractName}} 44 | -------------------------------------------------------------------------------- /support/wrap/templates/index.mustache: -------------------------------------------------------------------------------- 1 | {{#each wrappers}} 2 | import {{name}} from '{{path}}' 3 | {{/each}} 4 | 5 | 6 | export { 7 | {{#each wrappers}} 8 | {{name}}{{#unless @last}},{{/unless}} 9 | {{/each}} 10 | } 11 | -------------------------------------------------------------------------------- /test/scenarios/Cooperative.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import * as Web3 from 'web3' 3 | import * as asPromised from 'chai-as-promised' 4 | import * as contracts from '../../src/index' 5 | import * as support from '../support/index' 6 | import * as BigNumber from 'bignumber.js' 7 | import * as wrappers from '../../build/wrappers/index' 8 | import { InstantiationFactory } from '../support/index' 9 | 10 | 11 | chai.use(asPromised) 12 | 13 | const web3 = (global as any).web3 as Web3 14 | const assert = chai.assert 15 | 16 | const ECRecovery = artifacts.require('ECRecovery.sol') 17 | const Multisig = artifacts.require('Multisig.sol') 18 | const LibMultisig = artifacts.require('LibMultisig.sol') 19 | const LibCommon = artifacts.require('LibCommon.sol') 20 | 21 | const DistributeEth = artifacts.require('DistributeEth.sol') 22 | 23 | const TestToken = artifacts.require('TestToken.sol') 24 | const DistributeToken = artifacts.require('DistributeToken.sol') 25 | 26 | const amountA = new BigNumber.BigNumber(10) 27 | const amountB = new BigNumber.BigNumber(20) 28 | 29 | contract('Cooperative Behaviour', accounts => { 30 | let addressA = accounts[4] 31 | let addressB = accounts[5] 32 | 33 | let counterFactory: support.InstantiationFactory 34 | let multisig: contracts.Multisig.Contract 35 | 36 | let distributeEth: contracts.DistributeEth.Contract 37 | let token: wrappers.TestToken.Contract 38 | let distributeToken: contracts.DistributeToken.Contract 39 | 40 | before(async () => { 41 | Multisig.link(ECRecovery) 42 | LibMultisig.link(LibCommon) 43 | Multisig.link(LibMultisig) 44 | 45 | distributeEth = await DistributeEth.new() 46 | multisig = await Multisig.new(addressA, addressB) 47 | counterFactory = new InstantiationFactory(web3, multisig) 48 | 49 | token = await TestToken.new() 50 | await token.mint(multisig.address, amountB.plus(amountA)) 51 | await token.finishMinting() 52 | distributeToken = await DistributeToken.new() 53 | }) 54 | 55 | specify('distribute Ether', async () => { 56 | let transfer = distributeEth.execute.request(addressA, addressB, amountA, amountB) 57 | let command = await counterFactory.delegatecall(transfer) 58 | web3.eth.sendTransaction({from: accounts[0], to: multisig.address, value: amountB.plus(amountA)}) 59 | let beforeA = web3.eth.getBalance(addressA) 60 | let beforeB = web3.eth.getBalance(addressB) 61 | await multisig.doDelegate(command.destination, command.callBytecode, command.senderSig, command.receiverSig) 62 | let afterA = web3.eth.getBalance(addressA) 63 | let afterB = web3.eth.getBalance(addressB) 64 | assert.equal(afterA.minus(beforeA).toString(), amountA.toString()) 65 | assert.equal(afterB.minus(beforeB).toString(), amountB.toString()) 66 | assert.equal(web3.eth.getBalance(multisig.address).toNumber(), 0) 67 | }) 68 | 69 | specify('move Tokens', async () => { 70 | let nonce = (await multisig.state())[2] 71 | let transferTokens = distributeToken.execute.request(token.address, addressA, addressB, amountA, amountB) 72 | let command = await counterFactory.delegatecall(transferTokens, nonce) 73 | await support.assertTokenBalance(token, addressA, 0) 74 | await support.assertTokenBalance(token, addressB, 0) 75 | await support.assertTokenBalance(token, multisig.address, amountB.plus(amountA)) 76 | await multisig.doDelegate(command.destination, command.callBytecode, command.senderSig, command.receiverSig) 77 | await support.assertTokenBalance(token, addressA, amountA) 78 | await support.assertTokenBalance(token, addressB, amountB) 79 | await support.assertTokenBalance(token, multisig.address, 0) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /test/scenarios/Dispute.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import * as Web3 from 'web3' 3 | import * as asPromised from 'chai-as-promised' 4 | import * as contracts from '../../src/index' 5 | import * as support from '../support/index' 6 | import * as BigNumber from 'bignumber.js' 7 | import { InstantiationFactory, BytecodeManager, HexString, Address } from '../support/index' 8 | import * as util from 'ethereumjs-util' 9 | import MerkleTree from '../../src/MerkleTree' 10 | import * as wrappers from '../../build/wrappers' 11 | 12 | 13 | chai.use(asPromised) 14 | 15 | const web3 = (global as any).web3 as Web3 16 | const assert = chai.assert 17 | 18 | 19 | const ECRecovery = artifacts.require('ECRecovery.sol') 20 | const PublicRegistry = artifacts.require('PublicRegistry.sol') 21 | const Lineup = artifacts.require('Lineup.sol') 22 | const LibLineup = artifacts.require('LibLineup.sol') 23 | const Multisig = artifacts.require('Multisig.sol') 24 | const Bidirectional = artifacts.require('Bidirectional.sol') 25 | const Proxy = artifacts.require('Proxy.sol') 26 | const Conditional = artifacts.require('Conditional.sol') 27 | const LibBidirectional = artifacts.require('LibBidirectional.sol') 28 | const LibMultisig = artifacts.require('LibMultisig.sol') 29 | const LibCommon = artifacts.require('LibCommon.sol') 30 | 31 | Multisig.link(ECRecovery) 32 | Multisig.link(LibCommon) 33 | LibBidirectional.link(ECRecovery) 34 | LibMultisig.link(ECRecovery) 35 | Multisig.link(LibMultisig) 36 | Bidirectional.link(ECRecovery) 37 | Bidirectional.link(LibCommon) 38 | Bidirectional.link(LibBidirectional) 39 | LibLineup.link(LibCommon) 40 | Lineup.link(LibLineup) 41 | Lineup.link(LibCommon) 42 | 43 | interface LineupUpdate { 44 | nonce: BigNumber.BigNumber 45 | merkleRoot: HexString 46 | senderSig: HexString 47 | receiverSig: HexString, 48 | tree: MerkleTree 49 | } 50 | 51 | let _lineupNonce = 0 52 | let _lineupElements: Array = [] 53 | function updateLineup (element: HexString|null, sender: Address, receiver: Address): LineupUpdate { 54 | if (element) _lineupElements.push(element) 55 | let tree = new MerkleTree(_lineupElements.map(util.toBuffer)) 56 | _lineupNonce += 1 57 | let r = util.bufferToHex(tree.root) 58 | return { 59 | nonce: new BigNumber.BigNumber(_lineupNonce), 60 | merkleRoot: r, 61 | senderSig: support.lineupSign(sender, r, _lineupNonce), 62 | receiverSig: support.lineupSign(receiver, r, _lineupNonce), 63 | tree: tree 64 | } 65 | } 66 | 67 | function proof (lineupU: LineupUpdate, e: HexString): HexString { 68 | let tree = lineupU.tree 69 | let p = Buffer.concat(tree.proof(util.toBuffer(e))) 70 | return util.bufferToHex(p) 71 | } 72 | 73 | const lineupSettlementPeriod = 1 // Hint: Set to 0 to test lineup update period 74 | const bidirectionalSettlementPeriod = 10 75 | 76 | const REGISTRY_NONCE = '0x20' 77 | const depositA = new BigNumber.BigNumber(100) 78 | let amountA = new BigNumber.BigNumber(10) 79 | let amountB = new BigNumber.BigNumber(90) 80 | 81 | contract('Uncooperative Behaviour', accounts => { 82 | const addressA = accounts[3] 83 | const addressB = accounts[4] 84 | 85 | let lineupU = updateLineup(null, addressA, addressB) 86 | 87 | let registry: contracts.PublicRegistry.Contract 88 | let proxy: contracts.Proxy.Contract 89 | 90 | let bytecodeManager: BytecodeManager 91 | 92 | before(async () => { 93 | bytecodeManager = new BytecodeManager(web3) 94 | await bytecodeManager.addLink(ECRecovery, 'ECRecovery') 95 | await bytecodeManager.addLink(LibBidirectional, 'LibBidirectional') 96 | await bytecodeManager.addLink(LibCommon, 'LibCommon') 97 | await bytecodeManager.addLink(LibMultisig, 'LibMultisig') 98 | await bytecodeManager.addLink(LibLineup, 'LibLineup') 99 | 100 | registry = await PublicRegistry.deployed() 101 | proxy = await Proxy.deployed() 102 | }) 103 | 104 | specify('resolve dispute about Ether', async () => { 105 | // 1: Deploy Multisig 106 | let multisig = await Multisig.new(addressA, addressB) 107 | let counterFactory = new InstantiationFactory(web3, multisig) 108 | let conditional = await Conditional.new() 109 | 110 | // // 2: Counterfactually deploy Lineup 111 | let lineupB = bytecodeManager.constructBytecode(Lineup, 0x0, lineupSettlementPeriod, multisig.address) 112 | let lineupA = await registry.counterfactualAddress(lineupB, REGISTRY_NONCE) 113 | let lineupI = await counterFactory.call(registry.deploy.request(lineupB, REGISTRY_NONCE)) 114 | 115 | // 3: Prepare counterfactual deployment of Bidirectional, and update Lineup 116 | let bidirectionalB = bytecodeManager.constructBytecode(Bidirectional, multisig.address, bidirectionalSettlementPeriod) 117 | let bidirectionalA = await registry.counterfactualAddress(bidirectionalB, REGISTRY_NONCE) 118 | let bidirectionalDeployment = registry.deploy.request(bidirectionalB, REGISTRY_NONCE).params[0].data 119 | let bidirectionalCodehash = await conditional.callHash(registry.address, new BigNumber.BigNumber(0), bidirectionalDeployment) 120 | lineupU = updateLineup(bidirectionalCodehash, addressA, addressB) 121 | 122 | // 4. Prepare counterfactual transfer, and update Lineup 123 | let transferB = proxy.doCall.request(registry.address, bidirectionalA, depositA, '0x').params[0].data 124 | let transferCodehash = await conditional.callHash(proxy.address, depositA, transferB) 125 | lineupU = updateLineup(transferCodehash, addressA, addressB) 126 | 127 | // 5: Conditionally counterfactually deploy Bidirectional 128 | let conditionalBidirectionalB = conditional.doCall.request(registry.address, lineupA, proof(lineupU, bidirectionalCodehash), registry.address, new BigNumber.BigNumber(0), bidirectionalDeployment) 129 | let conditionalBidirectionalI = await counterFactory.call(conditionalBidirectionalB, 1) 130 | 131 | // 5. Conditionally counterfactually move money to deployed Bidirectional 132 | let conditionalTransferB = conditional.doDelegate.request(registry.address, lineupA, proof(lineupU, transferCodehash), proxy.address, depositA, transferB) 133 | let conditionalTransferI = await counterFactory.delegatecall(conditionalTransferB, 2) 134 | 135 | // 6. Deposit to Multisig 136 | web3.eth.sendTransaction({from: addressA, to: multisig.address, value: depositA}) 137 | 138 | // 5. Do exchanges on Bidirectional Eth channel 139 | // let nonce = new BigNumber.BigNumber(10) 140 | // let sigA = support.bidirectionalSign(addressA, nonce, amountA, amountB) 141 | // let sigB = support.bidirectionalSign(addressB, nonce, amountA, amountB) 142 | 143 | // 5.1 And close transaction for Bidirectional Eth channel 144 | let closeSigA = support.bidirectionalCloseSign(addressA, amountA, amountB) 145 | let closeSigB = support.bidirectionalCloseSign(addressB, amountA, amountB) 146 | 147 | // ****** And now Resolve the dispute for Bidirectional ****** \\ 148 | // 6. Deploy Lineup 149 | await counterFactory.execute(lineupI) 150 | let lineup = await Lineup.at(await registry.resolve(lineupA)) 151 | // 6.1 Optionally update Lineup 152 | await lineup.update(lineupU.nonce, lineupU.merkleRoot, lineupU.senderSig, lineupU.receiverSig) 153 | 154 | // 7 Deploy Bidirectional channel 155 | await counterFactory.execute(conditionalBidirectionalI) 156 | 157 | // 8 Move money to Bidirectional 158 | await counterFactory.execute(conditionalTransferI) 159 | assert.equal(web3.eth.getBalance(multisig.address).toNumber(), 0, 'Multisig balance') 160 | 161 | // 9 Close Bidirectional channel 162 | let bidirectional = await Bidirectional.at(await registry.resolve(bidirectionalA)) 163 | assert.equal(web3.eth.getBalance(bidirectional.address).toNumber(), depositA.toNumber(), 'Bidirectional balance') 164 | 165 | let addressABefore = web3.eth.getBalance(addressA) 166 | let addressBBefore = web3.eth.getBalance(addressB) 167 | await bidirectional.close(amountA, amountB, closeSigA, closeSigB) 168 | let addressAAfter = web3.eth.getBalance(addressA) 169 | let addressBAfter = web3.eth.getBalance(addressB) 170 | // Ensure the funds are distributed correctly 171 | assert.equal(web3.eth.getBalance(bidirectional.address).toNumber(), 0, 'Bidirectional balance after close') 172 | assert.equal(addressAAfter.minus(addressABefore).toNumber(), amountA.toNumber(), 'balance of addressA after close') 173 | assert.equal(addressBAfter.minus(addressBBefore).toNumber(), amountB.toNumber(), 'balance of addressB after close') 174 | }) 175 | }) 176 | -------------------------------------------------------------------------------- /test/support/index.ts: -------------------------------------------------------------------------------- 1 | import * as BigNumber from 'bignumber.js' 2 | import * as truffle from 'truffle-contract' 3 | import * as Web3 from 'web3' 4 | import { Multisig, PublicRegistry } from '../../src/index' 5 | import * as chai from 'chai' 6 | import * as abi from 'ethereumjs-abi' 7 | import * as util from 'ethereumjs-util' 8 | 9 | const web3 = (global as any).web3 as Web3 10 | 11 | const LOG_GAS_COST = Boolean(process.env.LOG_GAS_COST) 12 | const GAS_COST_IN_USD = 0.000012 // 1 ETH = 600 USD 13 | 14 | export type Number = BigNumber.BigNumber | number 15 | 16 | export class Gaser { 17 | web3: Web3 18 | 19 | constructor (_web3: Web3) { 20 | this.web3 = _web3 21 | } 22 | 23 | async gasDiff (name: string, account: string, fn: () => A, forceLog: boolean = false) { 24 | let before = web3.eth.getBalance(account) 25 | let result = fn() 26 | let after = web3.eth.getBalance(account) 27 | let gasCost = before.minus(after).div(this.web3.eth.gasPrice.div(0.2)).toNumber() 28 | this.log(gasCost, name, forceLog) 29 | return result 30 | } 31 | 32 | async logGas (name: string, promisedTx: Promise, forceLog: boolean = false) { 33 | let tx = await promisedTx 34 | this.log(tx.receipt.gasUsed, name, forceLog) 35 | return tx 36 | } 37 | 38 | private log (gasCost: number, name: string, forceLog: boolean = false) { 39 | if (LOG_GAS_COST || forceLog) { 40 | console.log(`GAS: ${name}: ($${(gasCost * GAS_COST_IN_USD).toFixed(2)})`, gasCost) 41 | } 42 | } 43 | } 44 | 45 | export async function logGas (name: string, promisedTx: Promise, forceLog: boolean = false) { 46 | let tx = await promisedTx 47 | if (LOG_GAS_COST || forceLog) { 48 | let gasCost = tx.receipt.gasUsed 49 | console.log(`GAS: ${name}: ($${(gasCost * GAS_COST_IN_USD).toFixed(2)})`, gasCost) 50 | } 51 | return tx 52 | } 53 | 54 | export async function gasDiff (name: string, web3: Web3, account: string, fn: () => A, forceLog: boolean = false) { 55 | let before = web3.eth.getBalance(account) 56 | 57 | // tslint:disable-next-line:await-promise 58 | let result = await fn() // TODO Do we need await here? 59 | let after = web3.eth.getBalance(account) 60 | if (LOG_GAS_COST || forceLog) { 61 | let gasCost = before.minus(after).div(web3.eth.gasPrice.div(0.2)).toString() // Beware of magic numbers 62 | console.log(`GAS: ${name}: ($${(parseFloat(gasCost) * GAS_COST_IN_USD).toFixed(2)})`, gasCost) 63 | } 64 | return result 65 | } 66 | 67 | export function txPrice (web3: Web3, log: truffle.TransactionResult): BigNumber.BigNumber { 68 | return web3.eth.getTransaction(log.tx).gasPrice.mul(log.receipt.gasUsed) 69 | } 70 | 71 | export function constructorBytecode (web3: Web3, contract: truffle.TruffleContract, ...params: any[]): string { 72 | return web3.eth.contract(contract.abi).getData(...params, {data: contract.bytecode}) 73 | } 74 | 75 | export function lineupSign (account: string, merkleRoot: HexString, nonce: Number) { 76 | let operationHash = util.bufferToHex(abi.soliditySHA3( 77 | ['bytes32', 'uint256'], 78 | [merkleRoot, nonce.toString()] 79 | )) 80 | return web3.eth.sign(account, operationHash) 81 | } 82 | 83 | export function bidirectionalSign (account: string, nonce: Number, toSender: Number, toReceiver: Number) { 84 | let operationHash = util.bufferToHex(abi.soliditySHA3( 85 | ['uint256', 'uint256', 'uint256'], 86 | [nonce.toString(), toSender.toString(), toReceiver.toString()] 87 | )) 88 | return web3.eth.sign(account, operationHash) 89 | } 90 | 91 | export function bidirectionalCloseSign (account: string, toSender: Number, toReceiver: Number) { 92 | let operationHash = util.bufferToHex(abi.soliditySHA3( 93 | ['string', 'uint256', 'uint256'], 94 | ['c', toSender.toString(), toReceiver.toString()] 95 | )) 96 | return web3.eth.sign(account, operationHash) 97 | } 98 | 99 | export interface CallParams { 100 | to: string 101 | data: string 102 | } 103 | 104 | export interface Call { 105 | method: string 106 | value?: BigNumber.BigNumber, 107 | params: Array 108 | } 109 | 110 | export type Address = string 111 | export type HexString = string 112 | 113 | export interface Instantiation { 114 | destination: Address 115 | callBytecode: HexString 116 | value: BigNumber.BigNumber 117 | operation: number, 118 | senderSig: HexString, 119 | receiverSig: HexString, 120 | nonce: BigNumber.BigNumber 121 | } 122 | 123 | export class InstantiationFactory { 124 | web3: Web3 125 | multisig: Multisig.Contract 126 | 127 | constructor (web3: Web3, multisig: Multisig.Contract) { 128 | this.web3 = web3 129 | this.multisig = multisig 130 | } 131 | 132 | async call (call: Call, _nonce?: BigNumber.BigNumber|number): Promise { 133 | return this.build(call, _nonce) 134 | } 135 | 136 | async delegatecall (call: Call, _nonce?: BigNumber.BigNumber|number): Promise { 137 | return this.buildDelegate(call, _nonce) 138 | } 139 | 140 | async execute (i: Instantiation, options?: Web3.TxData): Promise { 141 | if (i.operation === 0) { 142 | return this.multisig.doCall(i.destination, i.value, i.callBytecode, i.senderSig, i.receiverSig, options) 143 | } else { 144 | return this.multisig.doDelegate(i.destination, i.callBytecode, i.senderSig, i.receiverSig, options) 145 | } 146 | 147 | } 148 | 149 | async raw (destination: Address, value: BigNumber.BigNumber, callBytecode: HexString, nonce: BigNumber.BigNumber, operation: number) { 150 | let _state = await this.multisig.state() 151 | let sender = _state[0] 152 | let receiver = _state[1] 153 | 154 | let operationHash = util.bufferToHex(abi.soliditySHA3( 155 | ['address', 'address' , 'uint256', 'bytes', 'uint256'], 156 | [this.multisig.address, destination, value.toString(), util.toBuffer(callBytecode), nonce.toString()] 157 | )) // TODO Make it different for call and delegatecall 158 | let senderSig = this.web3.eth.sign(sender, operationHash) 159 | let receiverSig = this.web3.eth.sign(receiver, operationHash) 160 | 161 | return Promise.resolve({ 162 | destination, 163 | callBytecode, 164 | value, 165 | operation, 166 | senderSig, 167 | receiverSig, 168 | nonce 169 | }) 170 | } 171 | 172 | private async build (call: Call, _nonce?: BigNumber.BigNumber|number): Promise { 173 | let params = call.params[0] 174 | let value = new BigNumber.BigNumber(0) 175 | let _state = await this.multisig.state() 176 | let nonce = new BigNumber.BigNumber(_nonce || _state[2]) 177 | 178 | return this.raw(params.to, value, params.data, nonce, 0) 179 | } 180 | 181 | private async buildDelegate (call: Call, _nonce?: BigNumber.BigNumber|number): Promise { 182 | let params = call.params[0] 183 | let value = call.value || new BigNumber.BigNumber(0) 184 | let _state = await this.multisig.state() 185 | let nonce = new BigNumber.BigNumber(_nonce || _state[2]) 186 | let destination = params.to 187 | let callBytecode = params.data 188 | let operation = 1 189 | let sender = _state[0] 190 | let receiver = _state[1] 191 | 192 | let operationHash = util.bufferToHex(abi.soliditySHA3( 193 | ['address', 'address' , 'bytes', 'uint256'], 194 | [this.multisig.address, destination, util.toBuffer(callBytecode), nonce.toString()] 195 | )) // TODO Make it different for call and delegatecall 196 | let senderSig = this.web3.eth.sign(sender, operationHash) 197 | let receiverSig = this.web3.eth.sign(receiver, operationHash) 198 | 199 | return Promise.resolve({ 200 | destination, 201 | callBytecode, 202 | value, 203 | operation, 204 | senderSig, 205 | receiverSig, 206 | nonce 207 | }) 208 | } 209 | } 210 | 211 | export function counterfactualAddress (registry: PublicRegistry.Contract, i: Instantiation, nonce: HexString): Promise { 212 | let bytecode = i.callBytecode 213 | return registry.counterfactualAddress(bytecode, nonce) 214 | } 215 | 216 | export type ContractLike = { 217 | address: string 218 | constructor: { 219 | web3: Web3 220 | } 221 | } 222 | export async function assertBalance (contract: ContractLike, expected: BigNumber.BigNumber|number): Promise { 223 | let web3 = contract.constructor.web3 224 | let actualBalance = web3.eth.getBalance(contract.address) 225 | chai.assert.equal(actualBalance.toString(), expected.toString()) 226 | } 227 | 228 | export interface TokenLike { 229 | balanceOf (_owner: Address): Promise 230 | } 231 | export async function assertTokenBalance (token: TokenLike, address: Address, expected: BigNumber.BigNumber|number): Promise { 232 | let actualBalance = await token.balanceOf(address) 233 | chai.assert.equal(actualBalance.toString(), expected.toString()) 234 | } 235 | 236 | export class BytecodeManager { 237 | private _web3: Web3 238 | private _links: Map 239 | 240 | constructor (web3: Web3, links?: Map) { 241 | this._web3 = web3 242 | this._links = links ? links : new Map() 243 | } 244 | 245 | public constructBytecode (contract: truffle.TruffleContract, ...params: any[]): string { 246 | let bytecode = this._link(contract.bytecode) 247 | return this._web3.eth.contract(contract.abi).getData(...params, {data: bytecode}) 248 | } 249 | 250 | public async addLink (contract: truffle.TruffleContract, name: string) { 251 | let deployed = await contract.deployed() 252 | this._links.set(name, deployed.address) 253 | } 254 | 255 | protected _link (bytecode: string) { 256 | this._links.forEach((libraryAddress: string, libraryName: string) => { 257 | let regex = new RegExp('__' + libraryName + '_+', 'g') 258 | bytecode = bytecode.replace(regex, libraryAddress.replace('0x', '')) 259 | }) 260 | return bytecode 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /test/unit/Bidirectional.test.ts: -------------------------------------------------------------------------------- 1 | import * as Web3 from 'web3' 2 | import * as chai from 'chai' 3 | import * as BigNumber from 'bignumber.js' 4 | import * as asPromised from 'chai-as-promised' 5 | import * as contracts from '../../src/index' 6 | import * as support from '../support' 7 | 8 | chai.use(asPromised) 9 | 10 | const web3 = (global as any).web3 as Web3 11 | const assert = chai.assert 12 | const gaser = new support.Gaser(web3) 13 | 14 | const Multisig = artifacts.require('Multisig.sol') 15 | const LibMultisig = artifacts.require('LibMultisig.sol') 16 | const LibCommon = artifacts.require('LibCommon.sol') 17 | const Bidirectional = artifacts.require('Bidirectional.sol') 18 | const LibBidirectional = artifacts.require('LibBidirectional.sol') 19 | 20 | Multisig.link(LibCommon) 21 | Multisig.link(LibMultisig) 22 | Bidirectional.link(LibCommon) 23 | Bidirectional.link(LibBidirectional) 24 | 25 | let settlementPeriod = new BigNumber.BigNumber(10) 26 | 27 | contract('Bidirectional', accounts => { 28 | let multisig: contracts.Multisig.Contract 29 | let instance: contracts.Bidirectional.Contract 30 | 31 | let sender = accounts[2] 32 | let receiver = accounts[3] 33 | let toSender = new BigNumber.BigNumber(10) 34 | let toReceiver = new BigNumber.BigNumber(20) 35 | 36 | beforeEach(async () => { 37 | multisig = await Multisig.new(sender, receiver) 38 | instance = await Bidirectional.new(multisig.address, settlementPeriod) 39 | }) 40 | 41 | describe('.new', function () { 42 | specify('instantiate', async () => { 43 | let instance = await gaser.gasDiff('Bidirectional.new', sender, async () => { 44 | return await Bidirectional.new(multisig.address, settlementPeriod, {from: sender}) 45 | }) 46 | let state = await instance.state() 47 | assert.equal(state[0], multisig.address, 'multisig address') 48 | assert.equal(state[1].toNumber(), web3.eth.getBlock('latest').number, 'lastUpdate') 49 | assert.equal(state[2].toNumber(), settlementPeriod.toNumber(), 'settlementPeriod') 50 | assert.equal(state[3].toNumber(), 0, 'nonce') 51 | assert.equal(state[4].toNumber(), 0, 'toSender') 52 | assert.equal(state[5].toNumber(), 0, 'toReceiver') 53 | }) 54 | }) 55 | 56 | describe('.update', function () { 57 | specify('set new state', async () => { 58 | let nonce = new BigNumber.BigNumber(1) 59 | let senderSig = support.bidirectionalSign(sender, nonce, toSender, toReceiver) 60 | let receiverSig = support.bidirectionalSign(receiver, nonce, toSender, toReceiver) 61 | await gaser.logGas('Bidirectional.update', instance.update(nonce, toSender, toReceiver, senderSig, receiverSig)) 62 | let state = await instance.state() 63 | assert.equal(state[0], multisig.address, 'multisig address') 64 | assert.equal(state[1].toNumber(), web3.eth.getBlock('latest').number, 'lastUpdate') 65 | assert.equal(state[2].toNumber(), settlementPeriod.toNumber(), 'settlementPeriod') 66 | assert.equal(state[3].toNumber(), 1, 'nonce') 67 | assert.equal(state[4].toNumber(), toSender.toNumber(), 'toSender') 68 | assert.equal(state[5].toNumber(), toReceiver.toNumber(), 'toReceiver') 69 | }) 70 | specify('not if earlier nonce', async () => { 71 | let nonce = (await instance.state())[3] 72 | let senderSig = support.bidirectionalSign(sender, nonce, toSender, toReceiver) 73 | let receiverSig = support.bidirectionalSign(receiver, nonce, toSender, toReceiver) 74 | // tslint:disable-next-line:await-promise 75 | await assert.isRejected(instance.update(nonce, toSender, toReceiver, senderSig, receiverSig)) 76 | }) 77 | specify('not if late', async () => { 78 | let instance = await Bidirectional.new(multisig.address, 0, {from: sender}) 79 | let nonce = (await instance.state())[3].plus(1) 80 | let senderSig = support.bidirectionalSign(sender, nonce, toSender, toReceiver) 81 | let receiverSig = support.bidirectionalSign(receiver, nonce, toSender, toReceiver) 82 | web3.eth.sendTransaction({from: sender, to: receiver, value: 10}) 83 | // tslint:disable-next-line:await-promise 84 | await assert.isRejected(instance.update(nonce, toSender, toReceiver, senderSig, receiverSig)) 85 | }) 86 | specify('not if wrong signature', async () => { 87 | let nonce = (await instance.state())[3].plus(1) 88 | let senderSig = support.bidirectionalSign(sender, nonce, toSender, toReceiver) 89 | let receiverSig = support.bidirectionalSign(receiver, nonce, toSender, toReceiver) 90 | web3.eth.sendTransaction({from: sender, to: receiver, value: 10}) 91 | // tslint:disable-next-line:await-promise 92 | await assert.isRejected(instance.update(nonce, toSender, toReceiver, '0xdead', receiverSig)) 93 | // tslint:disable-next-line:await-promise 94 | await assert.isRejected(instance.update(nonce, toSender, toReceiver, senderSig, '0xdead')) 95 | }) 96 | }) 97 | 98 | describe('.close', () => { 99 | specify('destroy contract', async () => { 100 | let senderSig = support.bidirectionalCloseSign(sender, toSender, toReceiver) 101 | let receiverSig = support.bidirectionalCloseSign(receiver, toSender, toReceiver) 102 | web3.eth.sendTransaction({from: sender, to: instance.address, value: toSender.plus(toReceiver)}) 103 | await gaser.logGas('Bidirectional.close', instance.close(toSender, toReceiver, senderSig, receiverSig)) 104 | let state = await instance.state() 105 | assert.equal(state[0], '0x', 'multisig address') 106 | assert.equal(state[1].toNumber(), 0, 'lastUpdate') 107 | assert.equal(state[2].toNumber(), 0, 'settlementPeriod') 108 | assert.equal(state[3].toNumber(), 0, 'nonce') 109 | assert.equal(state[4].toNumber(), 0, 'toSender') 110 | assert.equal(state[5].toNumber(), 0, 'toReceiver') 111 | }) 112 | specify('transfer funds', async () => { 113 | let senderSig = support.bidirectionalCloseSign(sender, toSender, toReceiver) 114 | let receiverSig = support.bidirectionalCloseSign(receiver, toSender, toReceiver) 115 | web3.eth.sendTransaction({from: sender, to: instance.address, value: toSender.plus(toReceiver)}) 116 | let senderBefore = web3.eth.getBalance(sender) 117 | let receiverBefore = web3.eth.getBalance(receiver) 118 | await instance.close(toSender, toReceiver, senderSig, receiverSig) 119 | let senderAfter = web3.eth.getBalance(sender) 120 | let receiverAfter = web3.eth.getBalance(receiver) 121 | assert.equal(senderAfter.minus(senderBefore).toString(), toSender.toString()) 122 | assert.equal(receiverAfter.minus(receiverBefore).toString(), toReceiver.toString()) 123 | }) 124 | specify('not if no funds', async () => { 125 | let senderSig = support.bidirectionalCloseSign(sender, toSender, toReceiver) 126 | let receiverSig = support.bidirectionalCloseSign(receiver, toSender, toReceiver) 127 | // tslint:disable-next-line:await-promise 128 | await assert.isRejected(instance.close(toSender, toReceiver, senderSig, receiverSig)) 129 | }) 130 | specify('not if wrong signature', async () => { 131 | let senderSig = support.bidirectionalCloseSign(receiver, toSender, toReceiver) 132 | let receiverSig = support.bidirectionalCloseSign(receiver, toSender, toReceiver) 133 | web3.eth.sendTransaction({from: sender, to: instance.address, value: toSender.plus(toReceiver)}) 134 | // tslint:disable-next-line:await-promise 135 | await assert.isRejected(gaser.logGas('Bidirectional.close', instance.close(toSender, toReceiver, '0xdead', receiverSig))) 136 | // tslint:disable-next-line:await-promise 137 | await assert.isRejected(gaser.logGas('Bidirectional.close', instance.close(toSender, toReceiver, senderSig, '0xdead'))) 138 | }) 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /test/unit/Conditional.test.ts: -------------------------------------------------------------------------------- 1 | import * as Web3 from 'web3' 2 | import * as chai from 'chai' 3 | import * as BigNumber from 'bignumber.js' 4 | import * as asPromised from 'chai-as-promised' 5 | import * as contracts from '../../src/index' 6 | import * as util from 'ethereumjs-util' 7 | import * as truffle from 'truffle-contract' 8 | 9 | import TestContractWrapper from '../../build/wrappers/TestContract' 10 | import { InstantiationFactory, BytecodeManager } from '../support/index' 11 | import MerkleTree from '../../src/MerkleTree' 12 | 13 | chai.use(asPromised) 14 | 15 | const web3 = (global as any).web3 as Web3 16 | const assert = chai.assert 17 | 18 | const ECRecovery = artifacts.require('ECRecovery.sol') 19 | const PublicRegistry = artifacts.require('PublicRegistry.sol') 20 | const Multisig = artifacts.require('Multisig.sol') 21 | const Lineup = artifacts.require('Lineup.sol') 22 | const Conditional = artifacts.require('Conditional.sol') 23 | const LibCommon = artifacts.require('LibCommon.sol') 24 | const LibMultisig = artifacts.require('LibMultisig.sol') 25 | const LibLineup = artifacts.require('LibLineup.sol') 26 | 27 | const TestContract: truffle.TruffleContract = artifacts.require('TestContract.sol') 28 | 29 | 30 | contract('ConditionalCall', accounts => { 31 | let registry: contracts.PublicRegistry.Contract 32 | let conditional: contracts.Conditional.Contract 33 | let multisig: contracts.Multisig.Contract 34 | let counterFactory: InstantiationFactory 35 | let bytecodeManager: BytecodeManager 36 | 37 | let sender = accounts[0] 38 | let receiver = accounts[1] 39 | 40 | before(async () => { 41 | Multisig.link(ECRecovery) 42 | Multisig.link(LibCommon) 43 | Multisig.link(LibMultisig) 44 | multisig = await Multisig.new(sender, receiver) // TxCheck 45 | registry = await PublicRegistry.deployed() 46 | 47 | counterFactory = new InstantiationFactory(web3, multisig) 48 | conditional = await Conditional.new() 49 | 50 | bytecodeManager = new BytecodeManager(web3) 51 | await bytecodeManager.addLink(LibCommon, 'LibCommon') 52 | await bytecodeManager.addLink(LibLineup, 'LibLineup') 53 | }) 54 | 55 | let registryNonce = util.bufferToHex(Buffer.from('secret')) 56 | 57 | describe('.execute', () => { 58 | specify('call deployed contract', async () => { 59 | let testContract = await TestContract.new(1) 60 | assert.equal((await testContract.nonce()).toNumber(), 1) 61 | let newNonce = new BigNumber.BigNumber(10) 62 | 63 | let bytecode = testContract.updateNonce.request(newNonce).params[0].data 64 | let codeHash = await conditional.callHash(testContract.address, new BigNumber.BigNumber(0), bytecode) 65 | 66 | let merkleTree = new MerkleTree([util.toBuffer(codeHash)]) 67 | 68 | let lineupB = bytecodeManager.constructBytecode(Lineup, util.bufferToHex(merkleTree.root), 0, multisig.address) 69 | let lineupCAddress = await registry.counterfactualAddress(lineupB, registryNonce) 70 | let lineupI = await counterFactory.call(registry.deploy.request(lineupB, registryNonce)) 71 | await counterFactory.execute(lineupI) 72 | 73 | let proof = Buffer.concat(merkleTree.proof(util.toBuffer(codeHash))) // merkleTree.proof(codeHash) 74 | await conditional.doCall(registry.address, lineupCAddress, util.bufferToHex(proof), testContract.address, new BigNumber.BigNumber(0), bytecode) 75 | let actualNonce = await testContract.nonce() 76 | assert.equal(actualNonce.toNumber(), newNonce.toNumber()) 77 | }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /test/unit/Lineup.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import * as asPromised from 'chai-as-promised' 3 | import * as Web3 from 'web3' 4 | import * as BigNumber from 'bignumber.js' 5 | import * as util from 'ethereumjs-util' 6 | import * as contracts from '../../src/index' 7 | import * as support from '../support' 8 | import MerkleTree from '../../src/MerkleTree' 9 | 10 | chai.use(asPromised) 11 | 12 | const assert = chai.assert 13 | const web3 = (global as any).web3 as Web3 14 | const gaser = new support.Gaser(web3) 15 | 16 | const Lineup = artifacts.require('Lineup.sol') 17 | const Multisig = artifacts.require('Multisig.sol') 18 | const LibLineup = artifacts.require('LibLineup.sol') 19 | const LibMultisig = artifacts.require('LibMultisig.sol') 20 | const LibCommon = artifacts.require('LibCommon.sol') 21 | 22 | Multisig.link(LibCommon) 23 | Multisig.link(LibMultisig) 24 | Lineup.link(LibLineup) 25 | Lineup.link(LibCommon) 26 | 27 | const elements = [1, 2, 3].map(e => util.sha3(e)) 28 | const merkleTree = new MerkleTree(elements) 29 | 30 | contract('Lineup', accounts => { 31 | let lineup: contracts.Lineup.Contract 32 | let multisig: contracts.Multisig.Contract 33 | 34 | const sender = accounts[0] 35 | const receiver = accounts[1] 36 | 37 | function oneBlock () { 38 | web3.eth.sendTransaction({from: sender, to: receiver, value: 1}) // 1 block 39 | } 40 | 41 | before(async () => { 42 | multisig = await Multisig.new(sender, receiver, {from: sender}) 43 | lineup = await gaser.gasDiff('Lineup.new', sender, async () => { 44 | return await Lineup.new(0x0, 100, multisig.address, { from: sender }) 45 | }) 46 | }) 47 | 48 | describe('.update', async () => { 49 | let nonce = new BigNumber.BigNumber(42) 50 | let merkleRoot = util.bufferToHex(merkleTree.root) 51 | let senderSig = support.lineupSign(sender, merkleRoot, nonce) 52 | let receiverSig = support.lineupSign(receiver, merkleRoot, nonce) 53 | 54 | specify('set new state', async () => { 55 | await gaser.gasDiff('Lineup.update', sender, async () => { 56 | await lineup.update(nonce, merkleRoot, senderSig, receiverSig) 57 | }) 58 | let updatedNonce = (await lineup.state())[0] 59 | assert.equal(updatedNonce.toNumber(), nonce.toNumber()) 60 | let updatedMerkleRoot = (await lineup.state())[1] 61 | assert.equal(updatedMerkleRoot.toString(), merkleRoot) 62 | }) 63 | specify('not if late', async () => { 64 | let lineup = await Lineup.new(0x0, 0, multisig.address) 65 | oneBlock() 66 | // tslint:disable-next-line:await-promise 67 | await assert.isRejected(lineup.update(nonce, merkleRoot, senderSig, receiverSig)) 68 | }) 69 | specify('not if earlier nonce', async () => { 70 | // tslint:disable-next-line:await-promise 71 | await assert.isRejected(lineup.update(nonce.minus(1), merkleRoot, senderSig, receiverSig)) 72 | }) 73 | specify('not if not signed', async () => { 74 | // tslint:disable-next-line:await-promise 75 | await assert.isRejected(lineup.update(nonce.plus(1), merkleRoot, '0xdead', receiverSig)) 76 | // tslint:disable-next-line:await-promise 77 | await assert.isRejected(lineup.update(nonce.plus(1), merkleRoot, senderSig, '0xdead')) 78 | }) 79 | }) 80 | 81 | describe('.isContained', () => { 82 | let updatePeriod = new BigNumber.BigNumber(0) 83 | let merkleRoot = util.bufferToHex(merkleTree.root) 84 | let element = elements[0] 85 | let proof = util.bufferToHex(Buffer.concat(merkleTree.proof(element))) 86 | 87 | let lineup: contracts.Lineup.Contract 88 | 89 | beforeEach(async () => { 90 | lineup = await Lineup.new(merkleRoot, updatePeriod, multisig.address) 91 | }) 92 | 93 | specify('ok if contained', async () => { 94 | assert.isTrue(merkleTree.verify(merkleTree.proof(element), element)) 95 | assert.isTrue(await lineup.isContained(proof, util.bufferToHex(element))) 96 | }) 97 | specify('not if early', async () => { 98 | let lineup = await Lineup.new(merkleRoot, updatePeriod.plus(10), multisig.address) 99 | assert.isTrue(merkleTree.verify(merkleTree.proof(element), element)) 100 | assert.isFalse(await lineup.isContained(proof, util.bufferToHex(element))) 101 | }) 102 | specify('not if wrong proof', async () => { 103 | let fakeProof = util.toBuffer('0xdead') 104 | assert.isFalse(merkleTree.verify([fakeProof], element)) 105 | assert.isFalse(await lineup.isContained(util.bufferToHex(fakeProof), util.bufferToHex(element))) 106 | }) 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /test/unit/Multisig._test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import * as Web3 from 'web3' 3 | import * as asPromised from 'chai-as-promised' 4 | import { Address, BytecodeManager, Instantiation, InstantiationFactory } from '../support/index' 5 | import * as support from '../support/index' 6 | import * as BigNumber from 'bignumber.js' 7 | import * as contracts from '../../src/index' 8 | import * as util from 'ethereumjs-util' 9 | import * as wrappers from '../../build/wrappers' 10 | 11 | 12 | chai.use(asPromised) 13 | 14 | const web3 = (global as any).web3 as Web3 15 | const assert = chai.assert 16 | const gaser = new support.Gaser(web3) 17 | 18 | const FAKE_ADDRESS_A: Address = '0x0a00000000000000000000000000000000000000' 19 | const FAKE_ADDRESS_B: Address = '0x0b00000000000000000000000000000000000000' 20 | 21 | contract('Multisig', accounts => { 22 | let sender: string 23 | let receiver: string 24 | let alien: string 25 | 26 | let registryNonce: string 27 | 28 | const ECRecovery = artifacts.require('ECRecovery.sol') 29 | const LibCommon = artifacts.require('LibCommon.sol') 30 | const Multisig = artifacts.require('Multisig.sol') 31 | const LibMultisig = artifacts.require('LibMultisig.sol') 32 | const BidirectionalCF = artifacts.require('BidirectionalCF.sol') 33 | const BidirectionalCFLibrary = artifacts.require('BidirectionalCFLibrary.sol') 34 | const PublicRegistry = artifacts.require('PublicRegistry.sol') 35 | const Proxy = artifacts.require('Proxy.sol') 36 | const ProxyLibrary = artifacts.require('ProxyLibrary.sol') 37 | const Lineup = artifacts.require('Lineup.sol') 38 | const TestContract = artifacts.require('TestContract.sol') 39 | const TestToken = artifacts.require('TestToken.sol') 40 | const TransferToken = artifacts.require('TransferToken.sol') 41 | const DistributeEth = artifacts.require('DistributeEth.sol') 42 | const DistributeToken = artifacts.require('DistributeToken.sol') 43 | 44 | let bytecodeManager: BytecodeManager 45 | let counterFactory: InstantiationFactory 46 | let multisig: contracts.Multisig.Contract 47 | let proxy: contracts.Proxy.Contract 48 | let registry: contracts.PublicRegistry.Contract 49 | let transferToken: contracts.TransferToken.Contract 50 | let distributeEth: contracts.DistributeEth.Contract 51 | let distributeToken: contracts.DistributeToken.Contract 52 | 53 | 54 | 55 | before(async () => { 56 | sender = accounts[0] 57 | receiver = accounts[1] 58 | alien = accounts[2] 59 | 60 | Multisig.link(ECRecovery) 61 | Multisig.link(LibCommon) 62 | Multisig.link(LibMultisig) 63 | BidirectionalCFLibrary.link(ECRecovery) 64 | BidirectionalCF.link(LibCommon) 65 | BidirectionalCF.link(BidirectionalCFLibrary) 66 | Proxy.link(ProxyLibrary) 67 | Lineup.link(LibCommon) 68 | Lineup.link(BidirectionalCFLibrary) 69 | 70 | bytecodeManager = new BytecodeManager(web3) 71 | await bytecodeManager.addLink(ECRecovery, 'ECRecovery') 72 | await bytecodeManager.addLink(BidirectionalCFLibrary, 'BidirectionalCFLibrary') 73 | await bytecodeManager.addLink(LibCommon, 'LibCommon') 74 | await bytecodeManager.addLink(LibMultisig, 'LibMultisig') 75 | 76 | registry = await PublicRegistry.deployed() 77 | proxy = await Proxy.deployed() 78 | transferToken = await TransferToken.new() 79 | distributeEth = await DistributeEth.new() 80 | distributeToken = await DistributeToken.new() 81 | 82 | registryNonce = util.bufferToHex(Buffer.from('secret')) 83 | }) 84 | 85 | beforeEach(async () => { 86 | multisig = await Multisig.new(sender, receiver, {from: sender}) 87 | counterFactory = new InstantiationFactory(web3, multisig) 88 | }) 89 | 90 | describe('.new', () => { 91 | specify('have a valid sender', async () => { 92 | let _multisig = await gaser.gasDiff('Multisig.new', sender, async () => { 93 | return await Multisig.new(sender, receiver, { from: sender }) 94 | }) 95 | let actualSender = (await _multisig.state())[0] 96 | assert.equal(actualSender, sender, 'multisig must have a valid sender after construction') 97 | }) 98 | 99 | specify('have a valid receiver', async () => { 100 | let actualReceiver = (await multisig.state())[1] 101 | assert.equal(actualReceiver, receiver, 'multisig must have a valid receiver after construction') 102 | }) 103 | 104 | specify('have a valid nonce', async () => { 105 | let actualNonce = (await multisig.state())[2].toNumber() 106 | assert.equal(actualNonce, 0, 'multisig must have a valid nonce after construction') 107 | }) 108 | }) 109 | 110 | describe('.execute', () => { 111 | let amount = new BigNumber.BigNumber(100) 112 | 113 | specify('ok', async () => { 114 | web3.eth.sendTransaction({from: sender, to: multisig.address, value: amount}) 115 | 116 | let transfer = await counterFactory.raw(FAKE_ADDRESS_A, amount, '0x', new BigNumber.BigNumber(0), 0) 117 | let execution = multisig.doCall(transfer.destination, transfer.value, transfer.callBytecode, transfer.senderSig, transfer.receiverSig) 118 | // tslint:disable-next-line:await-promise 119 | await assert.isFulfilled(execution, 'transfer transaction') 120 | assert.equal(web3.eth.getBalance(FAKE_ADDRESS_A).toString(), amount.toString()) 121 | }) 122 | 123 | specify('not if wrong bytecode', async () => { 124 | let t = await counterFactory.raw(FAKE_ADDRESS_A, amount, '0xdead', new BigNumber.BigNumber(0), 0) 125 | // tslint:disable-next-line:await-promise 126 | await assert.isRejected(multisig.doCall(t.destination, t.value, t.callBytecode, t.senderSig, t.receiverSig)) 127 | }) 128 | 129 | specify('not if wrong senderSig', async () => { 130 | let t = await counterFactory.raw(FAKE_ADDRESS_A, amount, '0x', new BigNumber.BigNumber(0), 0) 131 | // tslint:disable-next-line:await-promise 132 | await assert.isRejected(multisig.doCall(t.destination, t.value, t.callBytecode, '0xdead', t.receiverSig)) 133 | }) 134 | 135 | specify('not if wrong receiverSig', async () => { 136 | let t = await counterFactory.raw(FAKE_ADDRESS_A, amount, '0x', new BigNumber.BigNumber(0), 0) 137 | // tslint:disable-next-line:await-promise 138 | await assert.isRejected(multisig.doCall(t.destination, t.value, t.callBytecode, t.senderSig, t.receiverSig)) 139 | }) 140 | }) 141 | 142 | describe('.executeDelegate', () => { 143 | let TestDelegatecallArtifact = artifacts.require('TestDelegatecall.sol') 144 | let testDelegatecall: wrappers.TestDelegatecall.Contract 145 | let amount = new BigNumber.BigNumber(100) 146 | let amountA = amount 147 | let amountB = amount.mul(2) 148 | 149 | before(async () => { 150 | testDelegatecall = await TestDelegatecallArtifact.new() 151 | }) 152 | 153 | beforeEach(() => { 154 | web3.eth.sendTransaction({from: sender, to: multisig.address, value: amountA.plus(amountB)}) 155 | }) 156 | 157 | specify('ok', async () => { 158 | let transfer = distributeEth.execute.request(FAKE_ADDRESS_A, FAKE_ADDRESS_B, amountA, amountB) 159 | let command = await counterFactory.delegatecall(transfer) 160 | let beforeA = web3.eth.getBalance(FAKE_ADDRESS_A) 161 | let beforeB = web3.eth.getBalance(FAKE_ADDRESS_B) 162 | // tslint:disable-next-line:await-promise 163 | await assert.isFulfilled(multisig.doDelegate(command.destination, command.callBytecode, command.senderSig, command.receiverSig)) 164 | let afterA = web3.eth.getBalance(FAKE_ADDRESS_A) 165 | let afterB = web3.eth.getBalance(FAKE_ADDRESS_B) 166 | assert.equal(afterA.minus(beforeA).toString(), amountA.toString()) 167 | assert.equal(afterB.minus(beforeB).toString(), amountB.toString()) 168 | }) 169 | 170 | specify('not if failed call', async () => { 171 | let call = testDelegatecall.execute.request(amountA, amountB) 172 | let command = await counterFactory.delegatecall(call) 173 | // tslint:disable-next-line:await-promise 174 | await assert.isRejected(multisig.doDelegate(command.destination, command.callBytecode, command.senderSig, command.receiverSig)) 175 | }) 176 | 177 | specify('not if wrong bytecode', async () => { 178 | let call = testDelegatecall.execute.request(amountA, amountB) 179 | call.params[0].data = '0xdead' 180 | let command = await counterFactory.delegatecall(call) 181 | // tslint:disable-next-line:await-promise 182 | await assert.isRejected(multisig.doDelegate(command.destination, command.callBytecode, command.senderSig, command.receiverSig)) 183 | }) 184 | 185 | specify('not if wrong signature', async () => { 186 | let call = testDelegatecall.execute.request(amountA, amountB) 187 | let command = await counterFactory.delegatecall(call) 188 | command.callBytecode = '0xdead' 189 | // tslint:disable-next-line:await-promise 190 | await assert.isRejected(multisig.doDelegate(command.destination, command.callBytecode, command.senderSig, command.receiverSig)) 191 | }) 192 | }) 193 | 194 | 195 | specify('can instantiate counterfactual contract', async () => { 196 | let probe = 42 197 | let bytecode = bytecodeManager.constructBytecode(TestContract, 42) 198 | let counterfactualAddress = await registry.counterfactualAddress(bytecode, registryNonce) 199 | 200 | // Instantiate 201 | let instantiation = await counterFactory.call(registry.deploy.request(bytecode, registryNonce)) 202 | await support.logGas('instantiate test contract', counterFactory.execute(instantiation)) 203 | 204 | // Check if instantiated 205 | let address = await registry.resolve(counterfactualAddress) 206 | let testInstance = await TestContract.at(address) 207 | let actualNonce = await testInstance.nonce() 208 | assert.equal(actualNonce.toNumber(), probe) 209 | }) 210 | 211 | specify('can send Eth to counterfactual contract', async () => { 212 | let multisig = await Multisig.new(sender, receiver, {from: sender}) 213 | let toMultisig = new BigNumber.BigNumber(web3.toWei(1, 'ether')) 214 | let toTestContract = new BigNumber.BigNumber(12) 215 | counterFactory = new InstantiationFactory(web3, multisig) 216 | 217 | let bytecodeTestContract = bytecodeManager.constructBytecode(TestContract, 1) 218 | let counterfactualAddress = await registry.counterfactualAddress(bytecodeTestContract, registryNonce) 219 | 220 | let instantiateTestContract = await counterFactory.call(registry.deploy.request(bytecodeTestContract, registryNonce)) 221 | let moveMoney = await counterFactory.delegatecall(proxy.doCall.request(registry.address, counterfactualAddress, toTestContract, '0x00'), instantiateTestContract.nonce.plus(1)) 222 | 223 | // Move Eth to Multisig 224 | 225 | await support.assertBalance(multisig, 0) 226 | web3.eth.sendTransaction({ from: sender, to: multisig.address, value: toMultisig }) // TxCheck 227 | await support.assertBalance(multisig, toMultisig) 228 | 229 | // Not Deployed Yet 230 | assert.equal(await registry.resolve(counterfactualAddress), '0x0000000000000000000000000000000000000000') 231 | 232 | await support.logGas('instantiate test contract', counterFactory.execute(instantiateTestContract)) 233 | 234 | let address = await registry.resolve(counterfactualAddress) 235 | let testInstance = await TestContract.at(address) 236 | 237 | await support.assertBalance(testInstance, 0) 238 | 239 | // Not moved Eth yet from Multisig to TestContract 240 | await support.assertBalance(multisig, toMultisig) 241 | await support.assertBalance(testInstance, 0) 242 | 243 | // Move Eth to TestContract 244 | await support.logGas('move Eth', counterFactory.execute(moveMoney)) 245 | await support.assertBalance(multisig, toMultisig.minus(toTestContract)) 246 | await support.assertBalance(testInstance, toTestContract) 247 | }) 248 | 249 | specify('can send ERC20 token to counterfactual contract', async () => { 250 | // Prepare 251 | let multisig = await Multisig.new(sender, receiver, {from: sender}) 252 | let initialMultisigBalance = new BigNumber.BigNumber(100) 253 | let toTestContract = new BigNumber.BigNumber(12) 254 | counterFactory = new InstantiationFactory(web3, multisig) 255 | 256 | let token = await TestToken.new() 257 | await token.mint(multisig.address, initialMultisigBalance) 258 | await token.finishMinting() 259 | 260 | // Deploy TestContract 261 | let bytecodeTestContract = bytecodeManager.constructBytecode(TestContract, 1) 262 | let counterfactualAddress = await registry.counterfactualAddress(bytecodeTestContract, registryNonce) 263 | let instantiateTestContract = await counterFactory.call(registry.deploy.request(bytecodeTestContract, registryNonce)) 264 | 265 | // Move tokens to contract 266 | let transferTokens = await counterFactory.delegatecall(transferToken.execute.request(registry.address, token.address, counterfactualAddress, toTestContract), instantiateTestContract.nonce.plus(1)) 267 | 268 | await support.assertTokenBalance(token, multisig.address, initialMultisigBalance) 269 | await support.logGas('instantiate test contract', counterFactory.execute(instantiateTestContract)) 270 | let testContractRealAddress = await registry.resolve(counterfactualAddress) 271 | await support.assertTokenBalance(token, multisig.address, initialMultisigBalance) 272 | await support.assertTokenBalance(token, testContractRealAddress, 0) 273 | 274 | await support.logGas('transfer tokens', counterFactory.execute(transferTokens)) 275 | await support.assertTokenBalance(token, multisig.address, initialMultisigBalance.minus(toTestContract)) 276 | await support.assertTokenBalance(token, testContractRealAddress, toTestContract) 277 | }) 278 | 279 | specify('can distribute Eth', async () => { 280 | let multisig = await Multisig.new(sender, receiver, {from: sender}) 281 | let toSender = new BigNumber.BigNumber(web3.toWei(3, 'ether')) 282 | let toReceiver = new BigNumber.BigNumber(web3.toWei(2, 'ether')) 283 | counterFactory = new InstantiationFactory(web3, multisig) 284 | let toMultisig = toSender.plus(toReceiver) 285 | 286 | let multisigBefore = web3.eth.getBalance(multisig.address) 287 | web3.eth.sendTransaction({ from: sender, to: multisig.address, value: toMultisig }) // TxCheck 288 | let multisigAfter = web3.eth.getBalance(multisig.address) 289 | assert.equal(multisigAfter.minus(multisigBefore).toString(), toMultisig.toString()) 290 | 291 | let distributeEthCommand = await counterFactory.delegatecall(distributeEth.execute.request(sender, receiver, toSender, toReceiver)) 292 | 293 | let senderBefore = web3.eth.getBalance(sender) 294 | let receiverBefore = web3.eth.getBalance(receiver) 295 | multisigBefore = web3.eth.getBalance(multisig.address) 296 | await support.logGas('distribute Eth', counterFactory.execute(distributeEthCommand, { from: alien })) 297 | let senderAfter = web3.eth.getBalance(sender) 298 | let receiverAfter = web3.eth.getBalance(receiver) 299 | multisigAfter = web3.eth.getBalance(multisig.address) 300 | assert.equal(senderAfter.minus(senderBefore).toString(), toSender.toString()) 301 | assert.equal(receiverAfter.minus(receiverBefore).toString(), toReceiver.toString()) 302 | assert.equal(multisigAfter.minus(multisigBefore).toString(), toMultisig.mul(-1).toString()) 303 | }) 304 | 305 | specify('can distribute ERC20 token', async () => { 306 | let multisig = await Multisig.new(sender, receiver, {from: sender}) 307 | let toSender = new BigNumber.BigNumber(web3.toWei(3, 'ether')) 308 | let toReceiver = new BigNumber.BigNumber(web3.toWei(2, 'ether')) 309 | counterFactory = new InstantiationFactory(web3, multisig) 310 | let toMultisig = toSender.plus(toReceiver) 311 | 312 | let token = await TestToken.new() 313 | await token.mint(multisig.address, toMultisig) 314 | await token.finishMinting() 315 | 316 | await support.assertTokenBalance(token, multisig.address, toMultisig) 317 | await support.assertTokenBalance(token, sender, 0) 318 | await support.assertTokenBalance(token, receiver, 0) 319 | 320 | let distributeTokenCommand = await counterFactory.delegatecall(distributeToken.execute.request(token.address, sender, receiver, toSender, toReceiver)) 321 | 322 | await support.logGas('distribute tokens', counterFactory.execute(distributeTokenCommand)) 323 | await support.assertTokenBalance(token, multisig.address, 0) 324 | await support.assertTokenBalance(token, sender, toSender) 325 | await support.assertTokenBalance(token, receiver, toReceiver) 326 | }) 327 | }) 328 | -------------------------------------------------------------------------------- /test/unit/Multisig.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import * as Web3 from 'web3' 3 | import * as asPromised from 'chai-as-promised' 4 | import { Address, BytecodeManager, InstantiationFactory } from '../support/index' 5 | import * as support from '../support/index' 6 | import * as BigNumber from 'bignumber.js' 7 | import * as contracts from '../../src/index' 8 | import * as wrappers from '../../build/wrappers' 9 | 10 | 11 | chai.use(asPromised) 12 | 13 | const web3 = (global as any).web3 as Web3 14 | const assert = chai.assert 15 | const gaser = new support.Gaser(web3) 16 | 17 | const FAKE_ADDRESS_A: Address = '0x0a00000000000000000000000000000000000000' 18 | const FAKE_ADDRESS_B: Address = '0x0b00000000000000000000000000000000000000' 19 | 20 | const ECRecovery = artifacts.require('ECRecovery.sol') 21 | const LibCommon = artifacts.require('LibCommon.sol') 22 | const Multisig = artifacts.require('Multisig.sol') 23 | const LibMultisig = artifacts.require('LibMultisig.sol') 24 | const DistributeEth = artifacts.require('DistributeEth.sol') 25 | 26 | Multisig.link(LibCommon) 27 | Multisig.link(LibMultisig) 28 | 29 | contract('Multisig', accounts => { 30 | let sender: string 31 | let receiver: string 32 | 33 | let counterFactory: InstantiationFactory 34 | let multisig: contracts.Multisig.Contract 35 | let distributeEth: contracts.DistributeEth.Contract 36 | 37 | let bytecodeManager = new BytecodeManager(web3) 38 | 39 | before(async () => { 40 | sender = accounts[0] 41 | receiver = accounts[1] 42 | 43 | await bytecodeManager.addLink(ECRecovery, 'ECRecovery') 44 | await bytecodeManager.addLink(LibCommon, 'LibCommon') 45 | await bytecodeManager.addLink(LibMultisig, 'LibMultisig') 46 | 47 | distributeEth = await DistributeEth.new() 48 | }) 49 | 50 | beforeEach(async () => { 51 | multisig = await Multisig.new(sender, receiver, {from: sender}) 52 | counterFactory = new InstantiationFactory(web3, multisig) 53 | }) 54 | 55 | describe('.new', () => { 56 | specify('have a valid sender', async () => { 57 | let _multisig = await gaser.gasDiff('Multisig.new', sender, async () => { 58 | return await Multisig.new(sender, receiver, { from: sender }) 59 | }) 60 | let actualSender = (await _multisig.state())[0] 61 | assert.equal(actualSender, sender, 'multisig must have a valid sender after construction') 62 | }) 63 | 64 | specify('have a valid receiver', async () => { 65 | let actualReceiver = (await multisig.state())[1] 66 | assert.equal(actualReceiver, receiver, 'multisig must have a valid receiver after construction') 67 | }) 68 | 69 | specify('have a valid nonce', async () => { 70 | let actualNonce = (await multisig.state())[2].toNumber() 71 | assert.equal(actualNonce, 0, 'multisig must have a valid nonce after construction') 72 | }) 73 | }) 74 | 75 | describe('.execute', () => { 76 | let amount = new BigNumber.BigNumber(100) 77 | 78 | specify('ok', async () => { 79 | web3.eth.sendTransaction({from: sender, to: multisig.address, value: amount}) 80 | 81 | let transfer = await counterFactory.raw(FAKE_ADDRESS_A, amount, '0x', new BigNumber.BigNumber(0), 0) 82 | let execution = multisig.doCall(transfer.destination, transfer.value, transfer.callBytecode, transfer.senderSig, transfer.receiverSig) 83 | // tslint:disable-next-line:await-promise 84 | await assert.isFulfilled(gaser.logGas('Multisig.execute: Transfer Ether', execution), 'transfer transaction') 85 | assert.equal(web3.eth.getBalance(FAKE_ADDRESS_A).toString(), amount.toString()) 86 | }) 87 | 88 | specify('not if wrong bytecode', async () => { 89 | let t = await counterFactory.raw(FAKE_ADDRESS_A, amount, '0xdead', new BigNumber.BigNumber(0), 0) 90 | // tslint:disable-next-line:await-promise 91 | await assert.isRejected(multisig.doCall(t.destination, t.value, t.callBytecode, t.senderSig, t.receiverSig)) 92 | }) 93 | 94 | specify('not if wrong senderSig', async () => { 95 | let t = await counterFactory.raw(FAKE_ADDRESS_A, amount, '0x', new BigNumber.BigNumber(0), 0) 96 | // tslint:disable-next-line:await-promise 97 | await assert.isRejected(multisig.doCall(t.destination, t.value, t.callBytecode, '0xdead', t.receiverSig)) 98 | }) 99 | 100 | specify('not if wrong receiverSig', async () => { 101 | let t = await counterFactory.raw(FAKE_ADDRESS_A, amount, '0x', new BigNumber.BigNumber(0), 0) 102 | // tslint:disable-next-line:await-promise 103 | await assert.isRejected(multisig.doCall(t.destination, t.value, t.callBytecode, t.senderSig, t.receiverSig)) 104 | }) 105 | }) 106 | 107 | describe('.executeDelegate', () => { 108 | let TestDelegatecallArtifact = artifacts.require('TestDelegatecall.sol') 109 | let testDelegatecall: wrappers.TestDelegatecall.Contract 110 | let amount = new BigNumber.BigNumber(100) 111 | let amountA = amount 112 | let amountB = amount.mul(2) 113 | 114 | before(async () => { 115 | testDelegatecall = await TestDelegatecallArtifact.new() 116 | }) 117 | 118 | beforeEach(() => { 119 | web3.eth.sendTransaction({from: sender, to: multisig.address, value: amountA.plus(amountB)}) 120 | }) 121 | 122 | specify('ok', async () => { 123 | let transfer = distributeEth.execute.request(FAKE_ADDRESS_A, FAKE_ADDRESS_B, amountA, amountB) 124 | let command = await counterFactory.delegatecall(transfer) 125 | let beforeA = web3.eth.getBalance(FAKE_ADDRESS_A) 126 | let beforeB = web3.eth.getBalance(FAKE_ADDRESS_B) 127 | let execution = multisig.doDelegate(command.destination, command.callBytecode, command.senderSig, command.receiverSig) 128 | // tslint:disable-next-line:await-promise 129 | await assert.isFulfilled(gaser.logGas('multisig.doDelegate: DistributeEth.execute', execution)) 130 | let afterA = web3.eth.getBalance(FAKE_ADDRESS_A) 131 | let afterB = web3.eth.getBalance(FAKE_ADDRESS_B) 132 | assert.equal(afterA.minus(beforeA).toString(), amountA.toString()) 133 | assert.equal(afterB.minus(beforeB).toString(), amountB.toString()) 134 | }) 135 | 136 | specify('not if failed call', async () => { 137 | let call = testDelegatecall.execute.request(amountA, amountB) 138 | let command = await counterFactory.delegatecall(call) 139 | // tslint:disable-next-line:await-promise 140 | await assert.isRejected(multisig.doDelegate(command.destination, command.callBytecode, command.senderSig, command.receiverSig)) 141 | }) 142 | 143 | specify('not if wrong bytecode', async () => { 144 | let call = testDelegatecall.execute.request(amountA, amountB) 145 | call.params[0].data = '0xdead' 146 | let command = await counterFactory.delegatecall(call) 147 | // tslint:disable-next-line:await-promise 148 | await assert.isRejected(multisig.doDelegate(command.destination, command.callBytecode, command.senderSig, command.receiverSig)) 149 | }) 150 | 151 | specify('not if wrong signature', async () => { 152 | let call = testDelegatecall.execute.request(amountA, amountB) 153 | let command = await counterFactory.delegatecall(call) 154 | command.callBytecode = '0xdead' 155 | // tslint:disable-next-line:await-promise 156 | await assert.isRejected(multisig.doDelegate(command.destination, command.callBytecode, command.senderSig, command.receiverSig)) 157 | }) 158 | }) 159 | }) 160 | -------------------------------------------------------------------------------- /test/unit/PublicRegistry.test.ts: -------------------------------------------------------------------------------- 1 | import * as Web3 from 'web3' 2 | import * as chai from 'chai' 3 | import * as BigNumber from 'bignumber.js' 4 | import * as asPromised from 'chai-as-promised' 5 | import * as contracts from '../../src/index' 6 | import * as util from 'ethereumjs-util' 7 | import * as support from '../support' 8 | import * as wrappers from '../../build/wrappers' 9 | 10 | chai.use(asPromised) 11 | 12 | const web3 = (global as any).web3 as Web3 13 | const assert = chai.assert 14 | const gaser = new support.Gaser(web3) 15 | 16 | const PublicRegistry = artifacts.require('PublicRegistry.sol') 17 | const TestContract = artifacts.require('TestContract.sol') 18 | 19 | contract('PublicRegistry', accounts => { 20 | let instance: contracts.PublicRegistry.Contract 21 | 22 | let sender = accounts[0] 23 | 24 | before(async () => { 25 | instance = await gaser.gasDiff('PublicRegistry.new', sender, async () => { 26 | return await PublicRegistry.new({ from: sender }) 27 | }) 28 | }) 29 | 30 | let nonce = util.addHexPrefix(new BigNumber.BigNumber(1).toString(16)) 31 | let probe = 42 32 | let bytecode = support.constructorBytecode(web3, TestContract, probe) 33 | 34 | describe('.deploy', () => { 35 | specify('instantiate the contract', async () => { 36 | await gaser.gasDiff('TestContract.new', sender, async () => { 37 | await TestContract.new(nonce) 38 | }) 39 | let tx = await gaser.logGas('PublicRegistry.deploy: TestContract.new', instance.deploy(bytecode, nonce, { from: accounts[0] })) 40 | let expectedId = await instance.counterfactualAddress(bytecode, nonce) 41 | assert.equal(tx.logs[0].args.id, expectedId) 42 | 43 | let address = tx.logs[0].args.deployed 44 | let testInstance = await TestContract.at(address) 45 | let actualNonce = await testInstance.nonce() 46 | assert.equal(actualNonce.toString(), probe.toString()) 47 | }) 48 | }) 49 | 50 | describe('.resolve', () => { 51 | specify('returns actual address', async () => { 52 | await instance.deploy(bytecode, nonce, { from: accounts[0] }) 53 | let counterfactualAddress = await instance.counterfactualAddress(bytecode, nonce) 54 | 55 | let address = await instance.resolve(counterfactualAddress) 56 | let testInstance = await TestContract.at(address) 57 | let actualNonce = await testInstance.nonce() 58 | assert.equal(actualNonce.toString(), probe.toString()) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | const GAS_LIMIT = 2700000 2 | 3 | module.exports = { 4 | networks: { 5 | development: { 6 | network_id: "*", 7 | host: "localhost", 8 | port: 8545, 9 | gas: GAS_LIMIT 10 | }, 11 | ropsten: { 12 | network_id: 3, 13 | host: "localhost", 14 | port: 8545, 15 | from: '0x1d612c43acf0c1bf855fe32a0beb782293b1bfb8', 16 | gas: GAS_LIMIT 17 | }, 18 | kovan: { 19 | network_id: 42, 20 | host: "localhost", 21 | port: 8545, 22 | gas: GAS_LIMIT 23 | }, 24 | main: { 25 | host: "localhost", 26 | port: 8545, 27 | network_id: 1, 28 | gas: GAS_LIMIT 29 | }, 30 | rinkeby: { 31 | host: "localhost", 32 | port: 8545, 33 | network_id: 4, 34 | from: '0x13d1be93e913d910245a069c67fc4c45a3d0b2fc', 35 | gas: GAS_LIMIT 36 | } 37 | }, 38 | solc: { 39 | optimizer: { 40 | enabled: true, 41 | runs: 200 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": [ "es2017" ], 6 | "sourceMap": true, // Generate corresponding '.map' file 7 | "declaration": true, // Generate corresponding '.d.ts' file 8 | "strict": true, // Enable all strict type-checking options 9 | "noImplicitReturns": true // Report error when not all code paths in function return a value 10 | }, 11 | "exclude": [ 12 | "node_modules/abstract-leveldown/index.d.ts" 13 | ], 14 | "include": [ 15 | "build/wrappers/*.ts", 16 | "node_modules/**/*/index.d.ts", 17 | "src/globals.d.ts", 18 | "src/**/*.ts", 19 | "migrations/**/*.ts", 20 | "test/**/*.ts", 21 | "types/**/*.d.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": "tslint-config-standard", 4 | "rules": { 5 | "no-consecutive-blank-lines": [false] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /types/ethereumjs-abi/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ethereumjs-abi' { 2 | export function soliditySHA3 (types: string[], values: any[]): Buffer 3 | export function rawEncode (types: string[], values: any[]): Buffer 4 | export function encodeSingle (type: string, value: any): Buffer 5 | export function rawDecode (types: string[], data: Buffer): A 6 | } 7 | --------------------------------------------------------------------------------