From ada5563b1fd2024ef310786eb8b1f32f3ecee4f0 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Fri, 31 Aug 2018 15:20:58 -0700 Subject: Update to most recent multisig --- .../src/2.0.0/multisig/MultiSigWallet.sol | 90 ++++++++++++++-------- .../2.0.0/multisig/MultiSigWalletWithTimeLock.sol | 55 ++++++------- .../protocol/AssetProxyOwner/AssetProxyOwner.sol | 15 ++-- 3 files changed, 89 insertions(+), 71 deletions(-) (limited to 'packages') diff --git a/packages/contracts/src/2.0.0/multisig/MultiSigWallet.sol b/packages/contracts/src/2.0.0/multisig/MultiSigWallet.sol index eb54fe047..516e7391c 100644 --- a/packages/contracts/src/2.0.0/multisig/MultiSigWallet.sol +++ b/packages/contracts/src/2.0.0/multisig/MultiSigWallet.sol @@ -1,13 +1,14 @@ // solhint-disable -pragma solidity ^0.4.10; +pragma solidity ^0.4.15; /// @title Multisignature wallet - Allows multiple parties to agree on transactions before execution. /// @author Stefan George - contract MultiSigWallet { - uint constant public MAX_OWNER_COUNT = 50; - + /* + * Events + */ event Confirmation(address indexed sender, uint indexed transactionId); event Revocation(address indexed sender, uint indexed transactionId); event Submission(uint indexed transactionId); @@ -18,6 +19,14 @@ contract MultiSigWallet { event OwnerRemoval(address indexed owner); event RequirementChange(uint required); + /* + * Constants + */ + uint constant public MAX_OWNER_COUNT = 50; + + /* + * Storage + */ mapping (uint => Transaction) public transactions; mapping (uint => mapping (address => bool)) public confirmations; mapping (address => bool) public isOwner; @@ -32,60 +41,54 @@ contract MultiSigWallet { bool executed; } + /* + * Modifiers + */ modifier onlyWallet() { - if (msg.sender != address(this)) - throw; + require(msg.sender == address(this)); _; } modifier ownerDoesNotExist(address owner) { - if (isOwner[owner]) - throw; + require(!isOwner[owner]); _; } modifier ownerExists(address owner) { - if (!isOwner[owner]) - throw; + require(isOwner[owner]); _; } modifier transactionExists(uint transactionId) { - if (transactions[transactionId].destination == 0) - throw; + require(transactions[transactionId].destination != 0); _; } modifier confirmed(uint transactionId, address owner) { - if (!confirmations[transactionId][owner]) - throw; + require(confirmations[transactionId][owner]); _; } modifier notConfirmed(uint transactionId, address owner) { - if (confirmations[transactionId][owner]) - throw; + require(!confirmations[transactionId][owner]); _; } modifier notExecuted(uint transactionId) { - if (transactions[transactionId].executed) - throw; + require(!transactions[transactionId].executed); _; } modifier notNull(address _address) { - if (_address == 0) - throw; + require(_address != 0); _; } modifier validRequirement(uint ownerCount, uint _required) { - if ( ownerCount > MAX_OWNER_COUNT - || _required > ownerCount - || _required == 0 - || ownerCount == 0) - throw; + require(ownerCount <= MAX_OWNER_COUNT + && _required <= ownerCount + && _required != 0 + && ownerCount != 0); _; } @@ -108,8 +111,7 @@ contract MultiSigWallet { validRequirement(_owners.length, _required) { for (uint i=0; i<_owners.length; i++) { - if (isOwner[_owners[i]] || _owners[i] == 0) - throw; + require(!isOwner[_owners[i]] && _owners[i] != 0); isOwner[_owners[i]] = true; } owners = _owners; @@ -151,7 +153,7 @@ contract MultiSigWallet { /// @dev Allows to replace an owner with a new owner. Transaction has to be sent by wallet. /// @param owner Address of owner to be replaced. - /// @param owner Address of new owner. + /// @param newOwner Address of new owner. function replaceOwner(address owner, address newOwner) public onlyWallet @@ -222,20 +224,44 @@ contract MultiSigWallet { /// @param transactionId Transaction ID. function executeTransaction(uint transactionId) public + ownerExists(msg.sender) + confirmed(transactionId, msg.sender) notExecuted(transactionId) { if (isConfirmed(transactionId)) { - Transaction tx = transactions[transactionId]; - tx.executed = true; - if (tx.destination.call.value(tx.value)(tx.data)) + Transaction storage txn = transactions[transactionId]; + txn.executed = true; + if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) Execution(transactionId); else { ExecutionFailure(transactionId); - tx.executed = false; + txn.executed = false; } } } + // call has been separated into its own function in order to take advantage + // of the Solidity's code generator to produce a loop that copies tx.data into memory. + function external_call(address destination, uint value, uint dataLength, bytes data) internal returns (bool) { + bool result; + assembly { + let x := mload(0x40) // "Allocate" memory for output (0x40 is where "free memory" pointer is stored by convention) + let d := add(data, 32) // First 32 bytes are the padded length of data, so exclude that + result := call( + sub(gas, 34710), // 34710 is the value that solidity is currently emitting + // It includes callGas (700) + callVeryLow (3, to pay for SUB) + callValueTransferGas (9000) + + // callNewAccountGas (25000, in case the destination address does not exist and needs creating) + destination, + value, + d, + dataLength, // Size of the input (in bytes) - this is what fixes the padding problem + x, + 0 // Output is ignored, therefore the output size is zero + ) + } + return result; + } + /// @dev Returns the confirmation status of a transaction. /// @param transactionId Transaction ID. /// @return Confirmation status. @@ -364,4 +390,4 @@ contract MultiSigWallet { for (i=from; i -contract MultiSigWalletWithTimeLock is MultiSigWallet { +contract MultiSigWalletWithTimeLock is + MultiSigWallet +{ - event ConfirmationTimeSet(uint indexed transactionId, uint confirmationTime); - event TimeLockChange(uint secondsTimeLocked); + event ConfirmationTimeSet(uint256 indexed transactionId, uint256 confirmationTime); + event TimeLockChange(uint256 secondsTimeLocked); - uint public secondsTimeLocked; + uint256 public secondsTimeLocked; - mapping (uint => uint) public confirmationTimes; + mapping (uint256 => uint256) public confirmationTimes; - modifier notFullyConfirmed(uint transactionId) { + modifier notFullyConfirmed(uint256 transactionId) { require(!isConfirmed(transactionId)); _; } - modifier fullyConfirmed(uint transactionId) { + modifier fullyConfirmed(uint256 transactionId) { require(isConfirmed(transactionId)); _; } - modifier pastTimeLock(uint transactionId) { + modifier pastTimeLock(uint256 transactionId) { require(block.timestamp >= confirmationTimes[transactionId] + secondsTimeLocked); _; } @@ -56,7 +58,11 @@ contract MultiSigWalletWithTimeLock is MultiSigWallet { /// @param _owners List of initial owners. /// @param _required Number of required confirmations. /// @param _secondsTimeLocked Duration needed after a transaction is confirmed and before it becomes executable, in seconds. - function MultiSigWalletWithTimeLock(address[] _owners, uint _required, uint _secondsTimeLocked) + function MultiSigWalletWithTimeLock( + address[] _owners, + uint256 _required, + uint256 _secondsTimeLocked + ) public MultiSigWallet(_owners, _required) { @@ -65,7 +71,7 @@ contract MultiSigWalletWithTimeLock is MultiSigWallet { /// @dev Changes the duration of the time lock for transactions. /// @param _secondsTimeLocked Duration needed after a transaction is confirmed and before it becomes executable, in seconds. - function changeTimeLock(uint _secondsTimeLocked) + function changeTimeLock(uint256 _secondsTimeLocked) public onlyWallet { @@ -75,7 +81,7 @@ contract MultiSigWalletWithTimeLock is MultiSigWallet { /// @dev Allows an owner to confirm a transaction. /// @param transactionId Transaction ID. - function confirmTransaction(uint transactionId) + function confirmTransaction(uint256 transactionId) public ownerExists(msg.sender) transactionExists(transactionId) @@ -89,34 +95,21 @@ contract MultiSigWalletWithTimeLock is MultiSigWallet { } } - /// @dev Allows an owner to revoke a confirmation for a transaction. - /// @param transactionId Transaction ID. - function revokeConfirmation(uint transactionId) - public - ownerExists(msg.sender) - confirmed(transactionId, msg.sender) - notExecuted(transactionId) - notFullyConfirmed(transactionId) - { - confirmations[transactionId][msg.sender] = false; - Revocation(msg.sender, transactionId); - } - /// @dev Allows anyone to execute a confirmed transaction. /// @param transactionId Transaction ID. - function executeTransaction(uint transactionId) + function executeTransaction(uint256 transactionId) public notExecuted(transactionId) fullyConfirmed(transactionId) pastTimeLock(transactionId) { - Transaction storage tx = transactions[transactionId]; - tx.executed = true; - if (tx.destination.call.value(tx.value)(tx.data)) + Transaction storage txn = transactions[transactionId]; + txn.executed = true; + if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) Execution(transactionId); else { ExecutionFailure(transactionId); - tx.executed = false; + txn.executed = false; } } @@ -125,7 +118,7 @@ contract MultiSigWalletWithTimeLock is MultiSigWallet { */ /// @dev Sets the time of when a submission first passed. - function setConfirmationTime(uint transactionId, uint confirmationTime) + function setConfirmationTime(uint256 transactionId, uint256 confirmationTime) internal { confirmationTimes[transactionId] = confirmationTime; diff --git a/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol b/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol index 5f69198e9..6ec710f25 100644 --- a/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol +++ b/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol @@ -38,13 +38,13 @@ contract AssetProxyOwner is /// @dev Function will revert if the transaction does not call `removeAuthorizedAddressAtIndex` /// on an approved AssetProxy contract. modifier validRemoveAuthorizedAddressAtIndexTx(uint256 transactionId) { - Transaction storage tx = transactions[transactionId]; + Transaction storage txn = transactions[transactionId]; require( - isAssetProxyRegistered[tx.destination], + isAssetProxyRegistered[txn.destination], "UNREGISTERED_ASSET_PROXY" ); require( - tx.data.readBytes4(0) == REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR, + txn.data.readBytes4(0) == REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR, "INVALID_FUNCTION_SELECTOR" ); _; @@ -96,14 +96,13 @@ contract AssetProxyOwner is fullyConfirmed(transactionId) validRemoveAuthorizedAddressAtIndexTx(transactionId) { - Transaction storage tx = transactions[transactionId]; - tx.executed = true; - // solhint-disable-next-line avoid-call-value - if (tx.destination.call.value(tx.value)(tx.data)) + Transaction storage txn = transactions[transactionId]; + txn.executed = true; + if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) emit Execution(transactionId); else { emit ExecutionFailure(transactionId); - tx.executed = false; + txn.executed = false; } } } -- cgit v1.2.3 From 3f1586045c61024d03a338a7d464e77d9af526b2 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Fri, 31 Aug 2018 15:59:03 -0700 Subject: Add test for executeRemoveAuthorizedAddressAtIndex when called by non-owner --- .../contracts/test/multisig/asset_proxy_owner.ts | 44 ++++++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) (limited to 'packages') diff --git a/packages/contracts/test/multisig/asset_proxy_owner.ts b/packages/contracts/test/multisig/asset_proxy_owner.ts index bb2b3b1a3..f8d66118a 100644 --- a/packages/contracts/test/multisig/asset_proxy_owner.ts +++ b/packages/contracts/test/multisig/asset_proxy_owner.ts @@ -34,6 +34,7 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); describe('AssetProxyOwner', () => { let owners: string[]; let authorized: string; + let notOwner: string; const REQUIRED_APPROVALS = new BigNumber(2); const SECONDS_TIME_LOCKED = new BigNumber(1000000); @@ -51,7 +52,9 @@ describe('AssetProxyOwner', () => { before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); owners = [accounts[0], accounts[1]]; - const initialOwner = (authorized = accounts[0]); + authorized = accounts[2]; + notOwner = accounts[3]; + const initialOwner = accounts[0]; erc20Proxy = await MixinAuthorizableContract.deployFrom0xArtifactAsync( artifacts.MixinAuthorizable, provider, @@ -395,7 +398,10 @@ describe('AssetProxyOwner', () => { ); }); - it('should execute removeAuthorizedAddressAtIndex for registered address if fully confirmed', async () => { + it('should execute removeAuthorizedAddressAtIndex for registered address if fully confirmed and called by owner', async () => { + const isAuthorizedBefore = await erc20Proxy.authorized.callAsync(authorized); + expect(isAuthorizedBefore).to.equal(true); + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( authorized, erc20Index, @@ -418,8 +424,38 @@ describe('AssetProxyOwner', () => { const isExecuted = tx[3]; expect(isExecuted).to.equal(true); - const isAuthorized = await erc20Proxy.authorized.callAsync(authorized); - expect(isAuthorized).to.equal(false); + const isAuthorizedAfter = await erc20Proxy.authorized.callAsync(authorized); + expect(isAuthorizedAfter).to.equal(false); + }); + + it('should execute removeAuthorizedAddressAtIndex for registered address if fully confirmed and called by non-owner', async () => { + const isAuthorizedBefore = await erc20Proxy.authorized.callAsync(authorized); + expect(isAuthorizedBefore).to.equal(true); + + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const submitRes = await multiSigWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const submitLog = submitRes.logs[0] as LogWithDecodedArgs; + const txId = submitLog.args.transactionId; + + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + + const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, notOwner); + const execLog = execRes.logs[1] as LogWithDecodedArgs; + expect(execLog.args.transactionId).to.be.bignumber.equal(txId); + + const tx = await testAssetProxyOwner.transactions.callAsync(txId); + const isExecuted = tx[3]; + expect(isExecuted).to.equal(true); + + const isAuthorizedAfter = await erc20Proxy.authorized.callAsync(authorized); + expect(isAuthorizedAfter).to.equal(false); }); it('should throw if already executed', async () => { -- cgit v1.2.3 From 9deec8ec353f21fcdbb8f7edb6a844fdf2ea9901 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sat, 1 Sep 2018 18:53:02 -0700 Subject: Add more tests for MultiSigWalletWithTimeLock --- .../test/multisig/multi_sig_with_time_lock.ts | 148 ++++++++++++++++++++- packages/contracts/test/utils/multi_sig_wrapper.ts | 5 + 2 files changed, 152 insertions(+), 1 deletion(-) (limited to 'packages') diff --git a/packages/contracts/test/multisig/multi_sig_with_time_lock.ts b/packages/contracts/test/multisig/multi_sig_with_time_lock.ts index 8eeeeca6b..d2da6a2a3 100644 --- a/packages/contracts/test/multisig/multi_sig_with_time_lock.ts +++ b/packages/contracts/test/multisig/multi_sig_with_time_lock.ts @@ -3,8 +3,13 @@ import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; import { LogWithDecodedArgs } from 'ethereum-types'; +import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; import { + MultiSigWalletWithTimeLockConfirmationEventArgs, + MultiSigWalletWithTimeLockConfirmationTimeSetEventArgs, MultiSigWalletWithTimeLockContract, + MultiSigWalletWithTimeLockExecutionEventArgs, + MultiSigWalletWithTimeLockExecutionFailureEventArgs, MultiSigWalletWithTimeLockSubmissionEventArgs, } from '../../generated_contract_wrappers/multi_sig_wallet_with_time_lock'; import { artifacts } from '../utils/artifacts'; @@ -21,6 +26,7 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); // tslint:disable:no-unnecessary-type-assertion describe('MultiSigWalletWithTimeLock', () => { let owners: string[]; + let notOwner: string; const REQUIRED_APPROVALS = new BigNumber(2); const SECONDS_TIME_LOCKED = new BigNumber(1000000); @@ -32,7 +38,8 @@ describe('MultiSigWalletWithTimeLock', () => { }); before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); - owners = [accounts[0], accounts[1]]; + owners = [accounts[0], accounts[1], accounts[2]]; + notOwner = accounts[3]; }); let multiSig: MultiSigWalletWithTimeLockContract; @@ -45,6 +52,145 @@ describe('MultiSigWalletWithTimeLock', () => { await blockchainLifecycle.revertAsync(); }); + describe('confirmTransaction', () => { + let txId: BigNumber; + beforeEach(async () => { + const secondsTimeLocked = new BigNumber(0); + multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync( + artifacts.MultiSigWalletWithTimeLock, + provider, + txDefaults, + owners, + REQUIRED_APPROVALS, + secondsTimeLocked, + ); + multiSigWrapper = new MultiSigWrapper(multiSig, provider); + const destination = notOwner; + const data = constants.NULL_BYTES; + const txReceipt = await multiSigWrapper.submitTransactionAsync(destination, data, owners[0]); + txId = (txReceipt.logs[0] as LogWithDecodedArgs).args + .transactionId; + }); + it('should revert if called by a non-owner', async () => { + await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.confirmTransactionAsync(txId, notOwner)); + }); + it('should revert if transaction does not exist', async () => { + const nonexistentTxId = new BigNumber(123456789); + await expectTransactionFailedWithoutReasonAsync( + multiSigWrapper.confirmTransactionAsync(nonexistentTxId, owners[1]), + ); + }); + it('should revert if transaction is already confirmed by caller', async () => { + await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.confirmTransactionAsync(txId, owners[0])); + }); + it('should confirm transaction for caller and log a Confirmation event', async () => { + const txReceipt = await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + const log = txReceipt.logs[0] as LogWithDecodedArgs; + expect(log.event).to.be.equal('Confirmation'); + expect(log.args.sender).to.be.equal(owners[1]); + expect(log.args.transactionId).to.be.bignumber.equal(txId); + }); + it('should revert if fully confirmed', async () => { + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.confirmTransactionAsync(txId, owners[2])); + }); + it('should set the confirmation time of the transaction if it becomes fully confirmed', async () => { + const blockNum = await web3Wrapper.getBlockNumberAsync(); + const timestamp = new BigNumber(await web3Wrapper.getBlockTimestampAsync(blockNum)); + const txReceipt = await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + const log = txReceipt.logs[1] as LogWithDecodedArgs; + expect(log.args.confirmationTime).to.be.bignumber.equal(timestamp); + expect(log.args.transactionId).to.be.bignumber.equal(txId); + }); + }); + describe('executeTransaction', () => { + let txId: BigNumber; + const secondsTimeLocked = new BigNumber(1000000); + beforeEach(async () => { + multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync( + artifacts.MultiSigWalletWithTimeLock, + provider, + txDefaults, + owners, + REQUIRED_APPROVALS, + secondsTimeLocked, + ); + multiSigWrapper = new MultiSigWrapper(multiSig, provider); + const destination = notOwner; + const data = constants.NULL_BYTES; + const txReceipt = await multiSigWrapper.submitTransactionAsync(destination, data, owners[0]); + txId = (txReceipt.logs[0] as LogWithDecodedArgs).args + .transactionId; + }); + it('should revert if transaction has not been fully confirmed', async () => { + await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.executeTransactionAsync(txId, owners[1])); + }); + it('should revert if time lock has not passed', async () => { + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.executeTransactionAsync(txId, owners[1])); + }); + it('should execute a transaction and log an Execution event if successful and called by owner', async () => { + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, owners[1]); + const log = txReceipt.logs[0] as LogWithDecodedArgs; + expect(log.event).to.be.equal('Execution'); + expect(log.args.transactionId).to.be.bignumber.equal(txId); + }); + it('should execute a transaction and log an Execution event if successful and called by non-owner', async () => { + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, notOwner); + const log = txReceipt.logs[0] as LogWithDecodedArgs; + expect(log.event).to.be.equal('Execution'); + expect(log.args.transactionId).to.be.bignumber.equal(txId); + }); + it('should revert if a required confirmation is revoked before executeTransaction is called', async () => { + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + await multiSigWrapper.revokeConfirmationAsync(txId, owners[0]); + await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.executeTransactionAsync(txId, owners[1])); + }); + it('should revert if transaction has been executed', async () => { + await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); + await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, owners[1]); + const log = txReceipt.logs[0] as LogWithDecodedArgs; + expect(log.args.transactionId).to.be.bignumber.equal(txId); + await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.executeTransactionAsync(txId, owners[1])); + }); + it("should log an ExecutionFailure event and not update the transaction's execution state if unsuccessful", async () => { + const contractWithoutFallback = await DummyERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyERC20Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + constants.DUMMY_TOKEN_DECIMALS, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + const data = constants.NULL_BYTES; + const value = new BigNumber(10); + const submissionTxReceipt = await multiSigWrapper.submitTransactionAsync( + contractWithoutFallback.address, + data, + owners[0], + { value }, + ); + const newTxId = (submissionTxReceipt.logs[0] as LogWithDecodedArgs< + MultiSigWalletWithTimeLockSubmissionEventArgs + >).args.transactionId; + await multiSigWrapper.confirmTransactionAsync(newTxId, owners[1]); + await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + const txReceipt = await multiSigWrapper.executeTransactionAsync(newTxId, owners[1]); + const executionFailureLog = txReceipt.logs[0] as LogWithDecodedArgs< + MultiSigWalletWithTimeLockExecutionEventArgs + >; + expect(executionFailureLog.event).to.be.equal('ExecutionFailure'); + expect(executionFailureLog.args.transactionId).to.be.bignumber.equal(newTxId); + }); + }); describe('changeTimeLock', () => { describe('initially non-time-locked', async () => { before(async () => { diff --git a/packages/contracts/test/utils/multi_sig_wrapper.ts b/packages/contracts/test/utils/multi_sig_wrapper.ts index e0c27b839..cb2b4c74c 100644 --- a/packages/contracts/test/utils/multi_sig_wrapper.ts +++ b/packages/contracts/test/utils/multi_sig_wrapper.ts @@ -36,6 +36,11 @@ export class MultiSigWrapper { const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); return tx; } + public async revokeConfirmationAsync(txId: BigNumber, from: string): Promise { + const txHash = await this._multiSig.revokeConfirmation.sendTransactionAsync(txId, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } public async executeTransactionAsync(txId: BigNumber, from: string): Promise { const txHash = await this._multiSig.executeTransaction.sendTransactionAsync(txId, { from, -- cgit v1.2.3 From 7a8ab6fbe806970176364b914790f2ce348194ff Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sat, 1 Sep 2018 19:11:00 -0700 Subject: Update MultiSigWalletWithTimeLock to 0.4.24 --- .../2.0.0/multisig/MultiSigWalletWithTimeLock.sol | 44 +++++++++++----------- .../protocol/AssetProxyOwner/AssetProxyOwner.sol | 4 +- 2 files changed, 24 insertions(+), 24 deletions(-) (limited to 'packages') diff --git a/packages/contracts/src/2.0.0/multisig/MultiSigWalletWithTimeLock.sol b/packages/contracts/src/2.0.0/multisig/MultiSigWalletWithTimeLock.sol index cb86f2cad..9513d3b30 100644 --- a/packages/contracts/src/2.0.0/multisig/MultiSigWalletWithTimeLock.sol +++ b/packages/contracts/src/2.0.0/multisig/MultiSigWalletWithTimeLock.sol @@ -16,18 +16,17 @@ */ -// solhint-disable -pragma solidity ^0.4.15; +pragma solidity 0.4.24; import "./MultiSigWallet.sol"; /// @title Multisignature wallet with time lock- Allows multiple parties to execute a transaction after a time lock has passed. /// @author Amir Bandeali - +// solhint-disable not-rely-on-time contract MultiSigWalletWithTimeLock is MultiSigWallet { - event ConfirmationTimeSet(uint256 indexed transactionId, uint256 confirmationTime); event TimeLockChange(uint256 secondsTimeLocked); @@ -36,29 +35,34 @@ contract MultiSigWalletWithTimeLock is mapping (uint256 => uint256) public confirmationTimes; modifier notFullyConfirmed(uint256 transactionId) { - require(!isConfirmed(transactionId)); + require( + !isConfirmed(transactionId), + "TX_FULLY_CONFIRMED" + ); _; } modifier fullyConfirmed(uint256 transactionId) { - require(isConfirmed(transactionId)); + require( + isConfirmed(transactionId), + "TX_NOT_FULLY_CONFIRMED" + ); _; } modifier pastTimeLock(uint256 transactionId) { - require(block.timestamp >= confirmationTimes[transactionId] + secondsTimeLocked); + require( + block.timestamp >= confirmationTimes[transactionId] + secondsTimeLocked, + "TIME_LOCK_INCOMPLETE" + ); _; } - /* - * Public functions - */ - /// @dev Contract constructor sets initial owners, required number of confirmations, and time lock. /// @param _owners List of initial owners. /// @param _required Number of required confirmations. /// @param _secondsTimeLocked Duration needed after a transaction is confirmed and before it becomes executable, in seconds. - function MultiSigWalletWithTimeLock( + constructor ( address[] _owners, uint256 _required, uint256 _secondsTimeLocked @@ -76,7 +80,7 @@ contract MultiSigWalletWithTimeLock is onlyWallet { secondsTimeLocked = _secondsTimeLocked; - TimeLockChange(_secondsTimeLocked); + emit TimeLockChange(_secondsTimeLocked); } /// @dev Allows an owner to confirm a transaction. @@ -89,7 +93,7 @@ contract MultiSigWalletWithTimeLock is notFullyConfirmed(transactionId) { confirmations[transactionId][msg.sender] = true; - Confirmation(msg.sender, transactionId); + emit Confirmation(msg.sender, transactionId); if (isConfirmed(transactionId)) { setConfirmationTime(transactionId, block.timestamp); } @@ -105,23 +109,19 @@ contract MultiSigWalletWithTimeLock is { Transaction storage txn = transactions[transactionId]; txn.executed = true; - if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) - Execution(transactionId); - else { - ExecutionFailure(transactionId); + if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) { + emit Execution(transactionId); + } else { + emit ExecutionFailure(transactionId); txn.executed = false; } } - /* - * Internal functions - */ - /// @dev Sets the time of when a submission first passed. function setConfirmationTime(uint256 transactionId, uint256 confirmationTime) internal { confirmationTimes[transactionId] = confirmationTime; - ConfirmationTimeSet(transactionId, confirmationTime); + emit ConfirmationTimeSet(transactionId, confirmationTime); } } diff --git a/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol b/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol index 6ec710f25..edb788fab 100644 --- a/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol +++ b/packages/contracts/src/2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol @@ -98,9 +98,9 @@ contract AssetProxyOwner is { Transaction storage txn = transactions[transactionId]; txn.executed = true; - if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) + if (external_call(txn.destination, txn.value, txn.data.length, txn.data)) { emit Execution(transactionId); - else { + } else { emit ExecutionFailure(transactionId); txn.executed = false; } -- cgit v1.2.3 From 652cf7a976444224b45e6271201ef9213a7059bd Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sat, 1 Sep 2018 19:40:44 -0700 Subject: Add MultiSigWalletWithTimeLock revert reasons to tests --- .../contracts/test/multisig/asset_proxy_owner.ts | 3 +- .../test/multisig/multi_sig_with_time_lock.ts | 43 +++++++++++++++------- packages/types/CHANGELOG.json | 4 ++ packages/types/src/index.ts | 3 ++ 4 files changed, 38 insertions(+), 15 deletions(-) (limited to 'packages') diff --git a/packages/contracts/test/multisig/asset_proxy_owner.ts b/packages/contracts/test/multisig/asset_proxy_owner.ts index f8d66118a..ee1ec74c4 100644 --- a/packages/contracts/test/multisig/asset_proxy_owner.ts +++ b/packages/contracts/test/multisig/asset_proxy_owner.ts @@ -345,10 +345,11 @@ describe('AssetProxyOwner', () => { const log = res.logs[0] as LogWithDecodedArgs; const txId = log.args.transactionId; - return expectTransactionFailedWithoutReasonAsync( + return expectTransactionFailedAsync( testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { from: owners[1], }), + RevertReason.TxNotFullyConfirmed, ); }); diff --git a/packages/contracts/test/multisig/multi_sig_with_time_lock.ts b/packages/contracts/test/multisig/multi_sig_with_time_lock.ts index d2da6a2a3..8496c6442 100644 --- a/packages/contracts/test/multisig/multi_sig_with_time_lock.ts +++ b/packages/contracts/test/multisig/multi_sig_with_time_lock.ts @@ -1,4 +1,5 @@ import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { RevertReason } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; import { LogWithDecodedArgs } from 'ethereum-types'; @@ -13,7 +14,7 @@ import { MultiSigWalletWithTimeLockSubmissionEventArgs, } from '../../generated_contract_wrappers/multi_sig_wallet_with_time_lock'; import { artifacts } from '../utils/artifacts'; -import { expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; +import { expectTransactionFailedAsync, expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; import { increaseTimeAndMineBlockAsync } from '../utils/block_timestamp'; import { chaiSetup } from '../utils/chai_setup'; import { constants } from '../utils/constants'; @@ -92,7 +93,10 @@ describe('MultiSigWalletWithTimeLock', () => { }); it('should revert if fully confirmed', async () => { await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); - await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.confirmTransactionAsync(txId, owners[2])); + await expectTransactionFailedAsync( + multiSigWrapper.confirmTransactionAsync(txId, owners[2]), + RevertReason.TxFullyConfirmed, + ); }); it('should set the confirmation time of the transaction if it becomes fully confirmed', async () => { const blockNum = await web3Wrapper.getBlockNumberAsync(); @@ -123,16 +127,22 @@ describe('MultiSigWalletWithTimeLock', () => { .transactionId; }); it('should revert if transaction has not been fully confirmed', async () => { - await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); - await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.executeTransactionAsync(txId, owners[1])); + await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber()); + await expectTransactionFailedAsync( + multiSigWrapper.executeTransactionAsync(txId, owners[1]), + RevertReason.TxNotFullyConfirmed, + ); }); it('should revert if time lock has not passed', async () => { await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); - await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.executeTransactionAsync(txId, owners[1])); + await expectTransactionFailedAsync( + multiSigWrapper.executeTransactionAsync(txId, owners[1]), + RevertReason.TimeLockIncomplete, + ); }); it('should execute a transaction and log an Execution event if successful and called by owner', async () => { await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); - await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber()); const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, owners[1]); const log = txReceipt.logs[0] as LogWithDecodedArgs; expect(log.event).to.be.equal('Execution'); @@ -140,7 +150,7 @@ describe('MultiSigWalletWithTimeLock', () => { }); it('should execute a transaction and log an Execution event if successful and called by non-owner', async () => { await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); - await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber()); const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, notOwner); const log = txReceipt.logs[0] as LogWithDecodedArgs; expect(log.event).to.be.equal('Execution'); @@ -148,13 +158,16 @@ describe('MultiSigWalletWithTimeLock', () => { }); it('should revert if a required confirmation is revoked before executeTransaction is called', async () => { await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); - await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber()); await multiSigWrapper.revokeConfirmationAsync(txId, owners[0]); - await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.executeTransactionAsync(txId, owners[1])); + await expectTransactionFailedAsync( + multiSigWrapper.executeTransactionAsync(txId, owners[1]), + RevertReason.TxNotFullyConfirmed, + ); }); it('should revert if transaction has been executed', async () => { await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); - await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber()); const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, owners[1]); const log = txReceipt.logs[0] as LogWithDecodedArgs; expect(log.args.transactionId).to.be.bignumber.equal(txId); @@ -182,10 +195,10 @@ describe('MultiSigWalletWithTimeLock', () => { MultiSigWalletWithTimeLockSubmissionEventArgs >).args.transactionId; await multiSigWrapper.confirmTransactionAsync(newTxId, owners[1]); - await web3Wrapper.increaseTimeAsync(secondsTimeLocked.toNumber()); + await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber()); const txReceipt = await multiSigWrapper.executeTransactionAsync(newTxId, owners[1]); const executionFailureLog = txReceipt.logs[0] as LogWithDecodedArgs< - MultiSigWalletWithTimeLockExecutionEventArgs + MultiSigWalletWithTimeLockExecutionFailureEventArgs >; expect(executionFailureLog.event).to.be.equal('ExecutionFailure'); expect(executionFailureLog.args.transactionId).to.be.bignumber.equal(newTxId); @@ -224,8 +237,9 @@ describe('MultiSigWalletWithTimeLock', () => { const res = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]); const log = res.logs[0] as LogWithDecodedArgs; const txId = log.args.transactionId; - return expectTransactionFailedWithoutReasonAsync( + return expectTransactionFailedAsync( multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), + RevertReason.TxNotFullyConfirmed, ); }); @@ -293,8 +307,9 @@ describe('MultiSigWalletWithTimeLock', () => { }); it('should throw if it has enough confirmations but is not past the time lock', async () => { - return expectTransactionFailedWithoutReasonAsync( + return expectTransactionFailedAsync( multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }), + RevertReason.TimeLockIncomplete, ); }); diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 0bd9e3b37..efbc78cbc 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Add AssetProxyOwner revert reasons", "pr": 1041 + }, + { + "note": "Add MultiSigWalletWithTimeLock revert reasons", + "pr": 1050 } ] }, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 3011dd87f..48a9e23d1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -227,6 +227,9 @@ export enum RevertReason { InvalidFunctionSelector = 'INVALID_FUNCTION_SELECTOR', InvalidAssetProxy = 'INVALID_ASSET_PROXY', UnregisteredAssetProxy = 'UNREGISTERED_ASSET_PROXY', + TxFullyConfirmed = 'TX_FULLY_CONFIRMED', + TxNotFullyConfirmed = 'TX_NOT_FULLY_CONFIRMED', + TimeLockIncomplete = 'TIME_LOCK_INCOMPLETE', } export enum StatusCodes { -- cgit v1.2.3 From e480e08aa44b3af0ef506278ce2018f8659e5971 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sat, 1 Sep 2018 20:01:10 -0700 Subject: Add test for external_call visibility --- .../contracts/test/multisig/multi_sig_with_time_lock.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'packages') diff --git a/packages/contracts/test/multisig/multi_sig_with_time_lock.ts b/packages/contracts/test/multisig/multi_sig_with_time_lock.ts index 8496c6442..bc1de7ed4 100644 --- a/packages/contracts/test/multisig/multi_sig_with_time_lock.ts +++ b/packages/contracts/test/multisig/multi_sig_with_time_lock.ts @@ -3,6 +3,7 @@ import { RevertReason } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; import { LogWithDecodedArgs } from 'ethereum-types'; +import * as _ from 'lodash'; import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token'; import { @@ -53,6 +54,20 @@ describe('MultiSigWalletWithTimeLock', () => { await blockchainLifecycle.revertAsync(); }); + describe('external_call', () => { + it('should be internal', async () => { + const secondsTimeLocked = new BigNumber(0); + multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync( + artifacts.MultiSigWalletWithTimeLock, + provider, + txDefaults, + owners, + REQUIRED_APPROVALS, + secondsTimeLocked, + ); + expect(_.isUndefined((multiSig as any).external_call)).to.be.equal(true); + }); + }); describe('confirmTransaction', () => { let txId: BigNumber; beforeEach(async () => { -- cgit v1.2.3 From f6b6619c08d5588ef5dfa653886f37c63018b477 Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 2 Sep 2018 10:36:28 -0700 Subject: Fix geth tests --- packages/contracts/test/multisig/asset_proxy_owner.ts | 8 ++++++-- packages/contracts/test/utils/multi_sig_wrapper.ts | 10 ++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) (limited to 'packages') diff --git a/packages/contracts/test/multisig/asset_proxy_owner.ts b/packages/contracts/test/multisig/asset_proxy_owner.ts index ee1ec74c4..299707512 100644 --- a/packages/contracts/test/multisig/asset_proxy_owner.ts +++ b/packages/contracts/test/multisig/asset_proxy_owner.ts @@ -272,8 +272,12 @@ describe('AssetProxyOwner', () => { await multiSigWrapper.confirmTransactionAsync(erc721AddAuthorizedAddressTxId, owners[1]); await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); await multiSigWrapper.executeTransactionAsync(registerAssetProxyTxId, owners[0]); - await multiSigWrapper.executeTransactionAsync(erc20AddAuthorizedAddressTxId, owners[0]); - await multiSigWrapper.executeTransactionAsync(erc721AddAuthorizedAddressTxId, owners[0]); + await multiSigWrapper.executeTransactionAsync(erc20AddAuthorizedAddressTxId, owners[0], { + gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + }); + await multiSigWrapper.executeTransactionAsync(erc721AddAuthorizedAddressTxId, owners[0], { + gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + }); }); describe('validRemoveAuthorizedAddressAtIndexTx', () => { diff --git a/packages/contracts/test/utils/multi_sig_wrapper.ts b/packages/contracts/test/utils/multi_sig_wrapper.ts index cb2b4c74c..e12a58695 100644 --- a/packages/contracts/test/utils/multi_sig_wrapper.ts +++ b/packages/contracts/test/utils/multi_sig_wrapper.ts @@ -6,7 +6,6 @@ import * as _ from 'lodash'; import { AssetProxyOwnerContract } from '../../generated_contract_wrappers/asset_proxy_owner'; import { MultiSigWalletContract } from '../../generated_contract_wrappers/multi_sig_wallet'; -import { constants } from './constants'; import { LogDecoder } from './log_decoder'; export class MultiSigWrapper { @@ -41,10 +40,14 @@ export class MultiSigWrapper { const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); return tx; } - public async executeTransactionAsync(txId: BigNumber, from: string): Promise { + public async executeTransactionAsync( + txId: BigNumber, + from: string, + opts: { gas?: number } = {}, + ): Promise { const txHash = await this._multiSig.executeTransaction.sendTransactionAsync(txId, { from, - gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + gas: opts.gas, }); const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); return tx; @@ -57,7 +60,6 @@ export class MultiSigWrapper { const txHash = await (this ._multiSig as AssetProxyOwnerContract).executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { from, - gas: constants.MAX_EXECUTE_TRANSACTION_GAS, }); const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); return tx; -- cgit v1.2.3 From e34b1f2f8b0aa32c95192c5c7fbbdecef2b3953a Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Sun, 2 Sep 2018 10:39:22 -0700 Subject: Remove TODOs that will not be addressed --- .../contracts/src/2.0.0/extensions/Forwarder/MixinExchangeWrapper.sol | 2 +- .../contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol | 2 +- packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) (limited to 'packages') diff --git a/packages/contracts/src/2.0.0/extensions/Forwarder/MixinExchangeWrapper.sol b/packages/contracts/src/2.0.0/extensions/Forwarder/MixinExchangeWrapper.sol index 33326c2ab..fea9a53c2 100644 --- a/packages/contracts/src/2.0.0/extensions/Forwarder/MixinExchangeWrapper.sol +++ b/packages/contracts/src/2.0.0/extensions/Forwarder/MixinExchangeWrapper.sol @@ -60,7 +60,7 @@ contract MixinExchangeWrapper is // Call `fillOrder` and handle any exceptions gracefully assembly { let success := call( - gas, // forward all gas, TODO: look into gas consumption of assert/throw + gas, // forward all gas exchange, // call address of Exchange contract 0, // transfer 0 wei add(fillOrderCalldata, 32), // pointer to start of input (skip array length in first 32 bytes) diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol index fc3210d90..cddff0e5f 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinWrapperFunctions.sol @@ -81,7 +81,7 @@ contract MixinWrapperFunctions is // Delegate to `fillOrder` and handle any exceptions gracefully assembly { let success := delegatecall( - gas, // forward all gas, TODO: look into gas consumption of assert/throw + gas, // forward all gas address, // call address of this contract add(fillOrderCalldata, 32), // pointer to start of input (skip array length in first 32 bytes) mload(fillOrderCalldata), // length of input diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol index d8fed176c..c0b85ea10 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/libs/LibMath.sol @@ -246,7 +246,6 @@ contract LibMath is numerator, denominator ); - // TODO: safeMod remainder = safeSub(denominator, remainder) % denominator; isError = safeMul(1000, remainder) >= safeMul(numerator, target); return isError; -- cgit v1.2.3