aboutsummaryrefslogtreecommitdiffstats
path: root/contracts/multisig
diff options
context:
space:
mode:
authorAmir Bandeali <abandeali1@gmail.com>2019-01-21 13:21:44 +0800
committerAmir Bandeali <abandeali1@gmail.com>2019-01-22 13:41:21 +0800
commit4a4c26a2e3a64f52f602c998095cf5d164ec0276 (patch)
tree5b3a50f7c6ec3cdb52e80cf779a5e94ee58d331b /contracts/multisig
parente2fe907de0e326e826e8ef3010efac8ec0136c74 (diff)
downloaddexon-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.json8
-rw-r--r--contracts/multisig/contracts/multisig/AssetProxyOwner.sol108
-rw-r--r--contracts/multisig/contracts/test/TestAssetProxyOwner.sol58
-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.json4
-rw-r--r--contracts/multisig/src/artifacts/index.ts6
-rw-r--r--contracts/multisig/test/asset_proxy_owner.ts507
-rw-r--r--contracts/multisig/test/utils/asset_proxy_owner_wrapper.ts71
-rw-r--r--contracts/multisig/tsconfig.json2
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"]