diff options
author | Amir Bandeali <abandeali1@gmail.com> | 2019-01-21 13:21:44 +0800 |
---|---|---|
committer | Amir Bandeali <abandeali1@gmail.com> | 2019-01-22 13:41:21 +0800 |
commit | 4a4c26a2e3a64f52f602c998095cf5d164ec0276 (patch) | |
tree | 5b3a50f7c6ec3cdb52e80cf779a5e94ee58d331b /contracts/multisig | |
parent | e2fe907de0e326e826e8ef3010efac8ec0136c74 (diff) | |
download | dexon-0x-contracts-4a4c26a2e3a64f52f602c998095cf5d164ec0276.tar.gz dexon-0x-contracts-4a4c26a2e3a64f52f602c998095cf5d164ec0276.tar.zst dexon-0x-contracts-4a4c26a2e3a64f52f602c998095cf5d164ec0276.zip |
Split protocol package into exchange, asset-proxy, and multisig
Diffstat (limited to 'contracts/multisig')
-rw-r--r-- | contracts/multisig/compiler.json | 8 | ||||
-rw-r--r-- | contracts/multisig/contracts/multisig/AssetProxyOwner.sol | 108 | ||||
-rw-r--r-- | contracts/multisig/contracts/test/TestAssetProxyOwner.sol | 58 | ||||
-rw-r--r-- | contracts/multisig/contracts/test/TestRejectEther.sol (renamed from contracts/multisig/contracts/test/TestRejectEther/TestRejectEther.sol) | 0 | ||||
-rw-r--r-- | contracts/multisig/package.json | 4 | ||||
-rw-r--r-- | contracts/multisig/src/artifacts/index.ts | 6 | ||||
-rw-r--r-- | contracts/multisig/test/asset_proxy_owner.ts | 507 | ||||
-rw-r--r-- | contracts/multisig/test/utils/asset_proxy_owner_wrapper.ts | 71 | ||||
-rw-r--r-- | contracts/multisig/tsconfig.json | 2 |
9 files changed, 761 insertions, 3 deletions
diff --git a/contracts/multisig/compiler.json b/contracts/multisig/compiler.json index 5a1f689e2..772665775 100644 --- a/contracts/multisig/compiler.json +++ b/contracts/multisig/compiler.json @@ -18,5 +18,11 @@ } } }, - "contracts": ["MultiSigWallet", "MultiSigWalletWithTimeLock", "TestRejectEther"] + "contracts": [ + "AssetProxyOwner", + "MultiSigWallet", + "MultiSigWalletWithTimeLock", + "TestAssetProxyOwner", + "TestRejectEther" + ] } diff --git a/contracts/multisig/contracts/multisig/AssetProxyOwner.sol b/contracts/multisig/contracts/multisig/AssetProxyOwner.sol new file mode 100644 index 000000000..e72918bf9 --- /dev/null +++ b/contracts/multisig/contracts/multisig/AssetProxyOwner.sol @@ -0,0 +1,108 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "@0x/contracts-multisig/contracts/multisig/MultiSigWalletWithTimeLock.sol"; +import "@0x/contracts-utils/contracts/utils/LibBytes.sol"; + + +contract AssetProxyOwner is + MultiSigWalletWithTimeLock +{ + using LibBytes for bytes; + + event AssetProxyRegistration(address assetProxyContract, bool isRegistered); + + // Mapping of AssetProxy contract address => + // if this contract is allowed to call the AssetProxy's `removeAuthorizedAddressAtIndex` method without a time lock. + mapping (address => bool) public isAssetProxyRegistered; + + bytes4 constant internal REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR = bytes4(keccak256("removeAuthorizedAddressAtIndex(address,uint256)")); + + /// @dev Function will revert if the transaction does not call `removeAuthorizedAddressAtIndex` + /// on an approved AssetProxy contract. + modifier validRemoveAuthorizedAddressAtIndexTx(uint256 transactionId) { + Transaction storage txn = transactions[transactionId]; + require( + isAssetProxyRegistered[txn.destination], + "UNREGISTERED_ASSET_PROXY" + ); + require( + txn.data.readBytes4(0) == REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR, + "INVALID_FUNCTION_SELECTOR" + ); + _; + } + + /// @dev Contract constructor sets initial owners, required number of confirmations, + /// time lock, and list of AssetProxy addresses. + /// @param _owners List of initial owners. + /// @param _assetProxyContracts Array of AssetProxy contract addresses. + /// @param _required Number of required confirmations. + /// @param _secondsTimeLocked Duration needed after a transaction is confirmed and before it becomes executable, in seconds. + constructor ( + address[] memory _owners, + address[] memory _assetProxyContracts, + uint256 _required, + uint256 _secondsTimeLocked + ) + public + MultiSigWalletWithTimeLock(_owners, _required, _secondsTimeLocked) + { + for (uint256 i = 0; i < _assetProxyContracts.length; i++) { + address assetProxy = _assetProxyContracts[i]; + require( + assetProxy != address(0), + "INVALID_ASSET_PROXY" + ); + isAssetProxyRegistered[assetProxy] = true; + } + } + + /// @dev Registers or deregisters an AssetProxy to be able to execute + /// `removeAuthorizedAddressAtIndex` without a timelock. + /// @param assetProxyContract Address of AssetProxy contract. + /// @param isRegistered Status of approval for AssetProxy contract. + function registerAssetProxy(address assetProxyContract, bool isRegistered) + public + onlyWallet + notNull(assetProxyContract) + { + isAssetProxyRegistered[assetProxyContract] = isRegistered; + emit AssetProxyRegistration(assetProxyContract, isRegistered); + } + + /// @dev Allows execution of `removeAuthorizedAddressAtIndex` without time lock. + /// @param transactionId Transaction ID. + function executeRemoveAuthorizedAddressAtIndex(uint256 transactionId) + public + notExecuted(transactionId) + fullyConfirmed(transactionId) + validRemoveAuthorizedAddressAtIndexTx(transactionId) + { + 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); + txn.executed = false; + } + } +} diff --git a/contracts/multisig/contracts/test/TestAssetProxyOwner.sol b/contracts/multisig/contracts/test/TestAssetProxyOwner.sol new file mode 100644 index 000000000..eb4c6d908 --- /dev/null +++ b/contracts/multisig/contracts/test/TestAssetProxyOwner.sol @@ -0,0 +1,58 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "../multisig/AssetProxyOwner.sol"; + + +// solhint-disable no-empty-blocks +contract TestAssetProxyOwner is + AssetProxyOwner +{ + constructor ( + address[] memory _owners, + address[] memory _assetProxyContracts, + uint256 _required, + uint256 _secondsTimeLocked + ) + public + AssetProxyOwner(_owners, _assetProxyContracts, _required, _secondsTimeLocked) + {} + + function testValidRemoveAuthorizedAddressAtIndexTx(uint256 id) + public + view + validRemoveAuthorizedAddressAtIndexTx(id) + returns (bool) + { + // Do nothing. We expect reverts through the modifier + return true; + } + + /// @dev Compares first 4 bytes of byte array to `removeAuthorizedAddressAtIndex` function selector. + /// @param data Transaction data. + /// @return Successful if data is a call to `removeAuthorizedAddressAtIndex`. + function isFunctionRemoveAuthorizedAddressAtIndex(bytes memory data) + public + pure + returns (bool) + { + return data.readBytes4(0) == REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR; + } +} diff --git a/contracts/multisig/contracts/test/TestRejectEther/TestRejectEther.sol b/contracts/multisig/contracts/test/TestRejectEther.sol index e523f591d..e523f591d 100644 --- a/contracts/multisig/contracts/test/TestRejectEther/TestRejectEther.sol +++ b/contracts/multisig/contracts/test/TestRejectEther.sol diff --git a/contracts/multisig/package.json b/contracts/multisig/package.json index 1c3911cbc..ec6bf5367 100644 --- a/contracts/multisig/package.json +++ b/contracts/multisig/package.json @@ -32,7 +32,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "generated-artifacts/@(MultiSigWallet|MultiSigWalletWithTimeLock|TestRejectEther).json" + "abis": "generated-artifacts/@(AssetProxyOwner|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestRejectEther).json" }, "repository": { "type": "git", @@ -70,6 +70,8 @@ }, "dependencies": { "@0x/base-contract": "^3.0.13", + "@0x/contracts-asset-proxy": "^2.2.3", + "@0x/contracts-tokens": "^1.0.6", "@0x/order-utils": "^3.1.2", "@0x/types": "^1.5.2", "@0x/typescript-typings": "^3.0.8", diff --git a/contracts/multisig/src/artifacts/index.ts b/contracts/multisig/src/artifacts/index.ts index 7cf47be01..326d79000 100644 --- a/contracts/multisig/src/artifacts/index.ts +++ b/contracts/multisig/src/artifacts/index.ts @@ -1,11 +1,15 @@ import { ContractArtifact } from 'ethereum-types'; +import * as AssetProxyOwner from '../../generated-artifacts/AssetProxyOwner.json'; import * as MultiSigWallet from '../../generated-artifacts/MultiSigWallet.json'; import * as MultiSigWalletWithTimeLock from '../../generated-artifacts/MultiSigWalletWithTimeLock.json'; +import * as TestAssetProxyOwner from '../../generated-artifacts/TestAssetProxyOwner.json'; import * as TestRejectEther from '../../generated-artifacts/TestRejectEther.json'; export const artifacts = { - TestRejectEther: TestRejectEther as ContractArtifact, + AssetProxyOwner: AssetProxyOwner as ContractArtifact, MultiSigWallet: MultiSigWallet as ContractArtifact, MultiSigWalletWithTimeLock: MultiSigWalletWithTimeLock as ContractArtifact, + TestAssetProxyOwner: TestAssetProxyOwner as ContractArtifact, + TestRejectEther: TestRejectEther as ContractArtifact, }; diff --git a/contracts/multisig/test/asset_proxy_owner.ts b/contracts/multisig/test/asset_proxy_owner.ts new file mode 100644 index 000000000..c7b7d997b --- /dev/null +++ b/contracts/multisig/test/asset_proxy_owner.ts @@ -0,0 +1,507 @@ +import { artifacts as proxyArtifacts, MixinAuthorizableContract } from '@0x/contracts-asset-proxy'; +import { + chaiSetup, + constants, + expectContractCallFailedAsync, + expectContractCreationFailedAsync, + expectTransactionFailedAsync, + expectTransactionFailedWithoutReasonAsync, + increaseTimeAndMineBlockAsync, + provider, + sendTransactionResult, + txDefaults, + web3Wrapper, +} from '@0x/contracts-test-utils'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { RevertReason } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import * as chai from 'chai'; +import { LogWithDecodedArgs } from 'ethereum-types'; + +import { + AssetProxyOwnerAssetProxyRegistrationEventArgs, + AssetProxyOwnerContract, + AssetProxyOwnerExecutionEventArgs, + AssetProxyOwnerExecutionFailureEventArgs, + AssetProxyOwnerSubmissionEventArgs, +} from '../generated-wrappers/asset_proxy_owner'; +import { TestAssetProxyOwnerContract } from '../generated-wrappers/test_asset_proxy_owner'; +import { artifacts } from '../src/artifacts'; + +import { AssetProxyOwnerWrapper } from './utils/asset_proxy_owner_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +// tslint:disable:no-unnecessary-type-assertion +describe('AssetProxyOwner', () => { + let owners: string[]; + let authorized: string; + let notOwner: string; + const REQUIRED_APPROVALS = new BigNumber(2); + const SECONDS_TIME_LOCKED = new BigNumber(1000000); + + let erc20Proxy: MixinAuthorizableContract; + let erc721Proxy: MixinAuthorizableContract; + let testAssetProxyOwner: TestAssetProxyOwnerContract; + let assetProxyOwnerWrapper: AssetProxyOwnerWrapper; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + owners = [accounts[0], accounts[1]]; + authorized = accounts[2]; + notOwner = accounts[3]; + const initialOwner = accounts[0]; + erc20Proxy = await MixinAuthorizableContract.deployFrom0xArtifactAsync( + proxyArtifacts.MixinAuthorizable, + provider, + txDefaults, + ); + erc721Proxy = await MixinAuthorizableContract.deployFrom0xArtifactAsync( + proxyArtifacts.MixinAuthorizable, + provider, + txDefaults, + ); + const defaultAssetProxyContractAddresses: string[] = []; + testAssetProxyOwner = await TestAssetProxyOwnerContract.deployFrom0xArtifactAsync( + artifacts.TestAssetProxyOwner, + provider, + txDefaults, + owners, + defaultAssetProxyContractAddresses, + REQUIRED_APPROVALS, + SECONDS_TIME_LOCKED, + ); + assetProxyOwnerWrapper = new AssetProxyOwnerWrapper(testAssetProxyOwner, provider); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.transferOwnership.sendTransactionAsync(testAssetProxyOwner.address, { + from: initialOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.transferOwnership.sendTransactionAsync(testAssetProxyOwner.address, { + from: initialOwner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('constructor', () => { + it('should register passed in assetProxyContracts', async () => { + const assetProxyContractAddresses = [erc20Proxy.address, erc721Proxy.address]; + const newMultiSig = await AssetProxyOwnerContract.deployFrom0xArtifactAsync( + artifacts.AssetProxyOwner, + provider, + txDefaults, + owners, + assetProxyContractAddresses, + REQUIRED_APPROVALS, + SECONDS_TIME_LOCKED, + ); + const isErc20ProxyRegistered = await newMultiSig.isAssetProxyRegistered.callAsync(erc20Proxy.address); + const isErc721ProxyRegistered = await newMultiSig.isAssetProxyRegistered.callAsync(erc721Proxy.address); + expect(isErc20ProxyRegistered).to.equal(true); + expect(isErc721ProxyRegistered).to.equal(true); + }); + it('should throw if a null address is included in assetProxyContracts', async () => { + const assetProxyContractAddresses = [erc20Proxy.address, constants.NULL_ADDRESS]; + return expectContractCreationFailedAsync( + (AssetProxyOwnerContract.deployFrom0xArtifactAsync( + artifacts.AssetProxyOwner, + provider, + txDefaults, + owners, + assetProxyContractAddresses, + REQUIRED_APPROVALS, + SECONDS_TIME_LOCKED, + ) as any) as sendTransactionResult, + RevertReason.InvalidAssetProxy, + ); + }); + }); + + describe('isFunctionRemoveAuthorizedAddressAtIndex', () => { + it('should return false if data is not for removeAuthorizedAddressAtIndex', async () => { + const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData( + owners[0], + ); + + const isFunctionRemoveAuthorizedAddressAtIndex = await testAssetProxyOwner.isFunctionRemoveAuthorizedAddressAtIndex.callAsync( + notRemoveAuthorizedAddressData, + ); + expect(isFunctionRemoveAuthorizedAddressAtIndex).to.be.false(); + }); + + it('should return true if data is for removeAuthorizedAddressAtIndex', async () => { + const index = new BigNumber(0); + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + owners[0], + index, + ); + const isFunctionRemoveAuthorizedAddressAtIndex = await testAssetProxyOwner.isFunctionRemoveAuthorizedAddressAtIndex.callAsync( + removeAuthorizedAddressAtIndexData, + ); + expect(isFunctionRemoveAuthorizedAddressAtIndex).to.be.true(); + }); + }); + + describe('registerAssetProxy', () => { + it('should throw if not called by multisig', async () => { + const isRegistered = true; + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, isRegistered, { + from: owners[0], + }), + ); + }); + + it('should register an address if called by multisig after timelock', async () => { + const addressToRegister = erc20Proxy.address; + const isRegistered = true; + const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData( + addressToRegister, + isRegistered, + ); + const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync( + testAssetProxyOwner.address, + registerAssetProxyData, + owners[0], + ); + + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]); + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + + const executeTxRes = await assetProxyOwnerWrapper.executeTransactionAsync(txId, owners[0]); + const registerLog = executeTxRes.logs[0] as LogWithDecodedArgs< + AssetProxyOwnerAssetProxyRegistrationEventArgs + >; + expect(registerLog.args.assetProxyContract).to.equal(addressToRegister); + expect(registerLog.args.isRegistered).to.equal(isRegistered); + + const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync( + addressToRegister, + ); + expect(isAssetProxyRegistered).to.equal(isRegistered); + }); + + it('should fail if registering a null address', async () => { + const addressToRegister = constants.NULL_ADDRESS; + const isRegistered = true; + const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData( + addressToRegister, + isRegistered, + ); + const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync( + testAssetProxyOwner.address, + registerAssetProxyData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]); + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + + const executeTxRes = await assetProxyOwnerWrapper.executeTransactionAsync(txId, owners[0]); + const failureLog = executeTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerExecutionFailureEventArgs>; + expect(failureLog.args.transactionId).to.be.bignumber.equal(txId); + + const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync( + addressToRegister, + ); + expect(isAssetProxyRegistered).to.equal(false); + }); + }); + + describe('Calling removeAuthorizedAddressAtIndex', () => { + const erc20Index = new BigNumber(0); + const erc721Index = new BigNumber(1); + before('authorize both proxies and register erc20 proxy', async () => { + // Only register ERC20 proxy + const addressToRegister = erc20Proxy.address; + const isRegistered = true; + const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData( + addressToRegister, + isRegistered, + ); + const registerAssetProxySubmitRes = await assetProxyOwnerWrapper.submitTransactionAsync( + testAssetProxyOwner.address, + registerAssetProxyData, + owners[0], + ); + const registerAssetProxySubmitLog = registerAssetProxySubmitRes.logs[0] as LogWithDecodedArgs< + AssetProxyOwnerSubmissionEventArgs + >; + + const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(authorized); + const erc20AddAuthorizedAddressSubmitRes = await assetProxyOwnerWrapper.submitTransactionAsync( + erc20Proxy.address, + addAuthorizedAddressData, + owners[0], + ); + const erc721AddAuthorizedAddressSubmitRes = await assetProxyOwnerWrapper.submitTransactionAsync( + erc721Proxy.address, + addAuthorizedAddressData, + owners[0], + ); + const erc20AddAuthorizedAddressSubmitLog = erc20AddAuthorizedAddressSubmitRes.logs[0] as LogWithDecodedArgs< + AssetProxyOwnerSubmissionEventArgs + >; + const erc721AddAuthorizedAddressSubmitLog = erc721AddAuthorizedAddressSubmitRes + .logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + + const registerAssetProxyTxId = registerAssetProxySubmitLog.args.transactionId; + const erc20AddAuthorizedAddressTxId = erc20AddAuthorizedAddressSubmitLog.args.transactionId; + const erc721AddAuthorizedAddressTxId = erc721AddAuthorizedAddressSubmitLog.args.transactionId; + + await assetProxyOwnerWrapper.confirmTransactionAsync(registerAssetProxyTxId, owners[1]); + await assetProxyOwnerWrapper.confirmTransactionAsync(erc20AddAuthorizedAddressTxId, owners[1]); + await assetProxyOwnerWrapper.confirmTransactionAsync(erc721AddAuthorizedAddressTxId, owners[1]); + await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber()); + await assetProxyOwnerWrapper.executeTransactionAsync(registerAssetProxyTxId, owners[0]); + await assetProxyOwnerWrapper.executeTransactionAsync(erc20AddAuthorizedAddressTxId, owners[0], { + gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + }); + await assetProxyOwnerWrapper.executeTransactionAsync(erc721AddAuthorizedAddressTxId, owners[0], { + gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + }); + }); + + describe('validRemoveAuthorizedAddressAtIndexTx', () => { + it('should revert if data is not for removeAuthorizedAddressAtIndex and proxy is registered', async () => { + const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData( + authorized, + ); + const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync( + erc20Proxy.address, + notRemoveAuthorizedAddressData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + return expectContractCallFailedAsync( + testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId), + RevertReason.InvalidFunctionSelector, + ); + }); + + it('should return true if data is for removeAuthorizedAddressAtIndex and proxy is registered', async () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + const isValidRemoveAuthorizedAddressAtIndexTx = await testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync( + txId, + ); + expect(isValidRemoveAuthorizedAddressAtIndexTx).to.be.true(); + }); + + it('should revert if data is for removeAuthorizedAddressAtIndex and proxy is not registered', async () => { + const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc721Index, + ); + const submitTxRes = await assetProxyOwnerWrapper.submitTransactionAsync( + erc721Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = submitTxRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + return expectContractCallFailedAsync( + testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId), + RevertReason.UnregisteredAssetProxy, + ); + }); + }); + + describe('executeRemoveAuthorizedAddressAtIndex', () => { + it('should throw without the required confirmations', async () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const res = await assetProxyOwnerWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + return expectTransactionFailedAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + RevertReason.TxNotFullyConfirmed, + ); + }); + + it('should throw if tx destination is not registered', async () => { + const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc721Index, + ); + const res = await assetProxyOwnerWrapper.submitTransactionAsync( + erc721Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]); + + return expectTransactionFailedAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + RevertReason.UnregisteredAssetProxy, + ); + }); + + it('should throw if tx data is not for removeAuthorizedAddressAtIndex', async () => { + const newAuthorized = owners[1]; + const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData( + newAuthorized, + ); + const res = await assetProxyOwnerWrapper.submitTransactionAsync( + erc20Proxy.address, + addAuthorizedAddressData, + owners[0], + ); + const log = res.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = log.args.transactionId; + + await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]); + + return expectTransactionFailedAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + RevertReason.InvalidFunctionSelector, + ); + }); + + 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, + ); + const submitRes = await assetProxyOwnerWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = submitLog.args.transactionId; + + await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]); + + const execRes = await assetProxyOwnerWrapper.executeRemoveAuthorizedAddressAtIndexAsync( + txId, + owners[0], + ); + const execLog = execRes.logs[1] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>; + 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 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 assetProxyOwnerWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = submitLog.args.transactionId; + + await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]); + + const execRes = await assetProxyOwnerWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, notOwner); + const execLog = execRes.logs[1] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>; + 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 () => { + const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData( + authorized, + erc20Index, + ); + const submitRes = await assetProxyOwnerWrapper.submitTransactionAsync( + erc20Proxy.address, + removeAuthorizedAddressAtIndexData, + owners[0], + ); + const submitLog = submitRes.logs[0] as LogWithDecodedArgs<AssetProxyOwnerSubmissionEventArgs>; + const txId = submitLog.args.transactionId; + + await assetProxyOwnerWrapper.confirmTransactionAsync(txId, owners[1]); + + const execRes = await assetProxyOwnerWrapper.executeRemoveAuthorizedAddressAtIndexAsync( + txId, + owners[0], + ); + const execLog = execRes.logs[1] as LogWithDecodedArgs<AssetProxyOwnerExecutionEventArgs>; + 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); + + return expectTransactionFailedWithoutReasonAsync( + testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, { + from: owners[1], + }), + ); + }); + }); + }); +}); +// tslint:disable-line max-file-line-count diff --git a/contracts/multisig/test/utils/asset_proxy_owner_wrapper.ts b/contracts/multisig/test/utils/asset_proxy_owner_wrapper.ts new file mode 100644 index 000000000..924a215db --- /dev/null +++ b/contracts/multisig/test/utils/asset_proxy_owner_wrapper.ts @@ -0,0 +1,71 @@ +import { artifacts as proxyArtifacts } from '@0x/contracts-asset-proxy'; +import { LogDecoder } from '@0x/contracts-test-utils'; +import { artifacts as tokensArtifacts } from '@0x/contracts-tokens'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { AssetProxyOwnerContract } from '../../generated-wrappers/asset_proxy_owner'; +import { artifacts } from '../../src/artifacts'; + +export class AssetProxyOwnerWrapper { + private readonly _assetProxyOwner: AssetProxyOwnerContract; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _logDecoder: LogDecoder; + constructor(assetproxyOwnerContract: AssetProxyOwnerContract, provider: Provider) { + this._assetProxyOwner = assetproxyOwnerContract; + this._web3Wrapper = new Web3Wrapper(provider); + this._logDecoder = new LogDecoder(this._web3Wrapper, { ...artifacts, ...tokensArtifacts, ...proxyArtifacts }); + } + public async submitTransactionAsync( + destination: string, + data: string, + from: string, + opts: { value?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const value = _.isUndefined(opts.value) ? new BigNumber(0) : opts.value; + const txHash = await this._assetProxyOwner.submitTransaction.sendTransactionAsync(destination, value, data, { + from, + }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async confirmTransactionAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._assetProxyOwner.confirmTransaction.sendTransactionAsync(txId, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async revokeConfirmationAsync(txId: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._assetProxyOwner.revokeConfirmation.sendTransactionAsync(txId, { from }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async executeTransactionAsync( + txId: BigNumber, + from: string, + opts: { gas?: number } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._assetProxyOwner.executeTransaction.sendTransactionAsync(txId, { + from, + gas: opts.gas, + }); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async executeRemoveAuthorizedAddressAtIndexAsync( + txId: BigNumber, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + // tslint:disable-next-line:no-unnecessary-type-assertion + const txHash = await (this + ._assetProxyOwner as AssetProxyOwnerContract).executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync( + txId, + { + from, + }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } +} diff --git a/contracts/multisig/tsconfig.json b/contracts/multisig/tsconfig.json index 6f381620e..ad1707e43 100644 --- a/contracts/multisig/tsconfig.json +++ b/contracts/multisig/tsconfig.json @@ -7,8 +7,10 @@ }, "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "files": [ + "./generated-artifacts/AssetProxyOwner.json", "./generated-artifacts/MultiSigWallet.json", "./generated-artifacts/MultiSigWalletWithTimeLock.json", + "./generated-artifacts/TestAssetProxyOwner.json", "./generated-artifacts/TestRejectEther.json" ], "exclude": ["./deploy/solc/solc_bin"] |