Merge development
This commit is contained in:
@@ -1,4 +1,17 @@
|
||||
[
|
||||
{
|
||||
"version": "3.0.0",
|
||||
"changes": [
|
||||
{
|
||||
"note": "Add AssetProxyOwner contract",
|
||||
"pr": 1539
|
||||
},
|
||||
{
|
||||
"note": "Rename multisig directory to src",
|
||||
"pr": 1539
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"changes": [
|
||||
|
17
contracts/multisig/DEPLOYS.json
Normal file
17
contracts/multisig/DEPLOYS.json
Normal file
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"name": "AssetProxyOwner",
|
||||
"version": "1.0.0",
|
||||
"changes": [
|
||||
{
|
||||
"note": "protocol v2 deploy",
|
||||
"networks": {
|
||||
"1": "0x17992e4ffb22730138e4b62aaa6367fa9d3699a6",
|
||||
"3": "0xf5fa5b5fed2727a0e44ac67f6772e97977aa358b",
|
||||
"4": "0x1da52d1d3a3acfa0a1836b737393b4e9931268fc",
|
||||
"42": "0x2c824d2882baa668e0d5202b1e7f2922278703f8"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@@ -1,15 +1,14 @@
|
||||
## MultisSig Contracts
|
||||
## MultiSignature Contracts
|
||||
|
||||
MultiSig smart contracts
|
||||
This package contains various types of multisignature wallet contracts, including the [`AssetProxyOwner`](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md#assetproxyowner) contract that is responsible for upgrading the 0x protocol smart contracts. Addresses of the deployed contracts can be found in the 0x [wiki](https://0xproject.com/wiki#Deployed-Addresses) or the [DEPLOYS](./DEPLOYS.json) file within this package.
|
||||
|
||||
## Usage
|
||||
## Installation
|
||||
|
||||
Contracts can be found in the [contracts](./contracts) directory. The contents of this directory are broken down into the following subdirectories:
|
||||
**Install**
|
||||
|
||||
- [multisig](./contracts/multisig)
|
||||
- This directory contains the [Gnosis MultiSigWallet](https://github.com/gnosis/MultiSigWallet) and a custom extension that adds a timelock to transactions within the MultiSigWallet.
|
||||
- [test](./contracts/test)
|
||||
- This directory contains mocks and other contracts that are used solely for testing contracts within the other directories.
|
||||
```bash
|
||||
npm install @0x/contracts-multisig --save
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@@ -19,5 +19,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contracts": ["MultiSigWallet", "MultiSigWalletWithTimeLock", "TestRejectEther"]
|
||||
"contracts": [
|
||||
"AssetProxyOwner",
|
||||
"MultiSigWallet",
|
||||
"MultiSigWalletWithTimeLock",
|
||||
"TestAssetProxyOwner",
|
||||
"TestRejectEther"
|
||||
]
|
||||
}
|
||||
|
108
contracts/multisig/contracts/src/AssetProxyOwner.sol
Normal file
108
contracts/multisig/contracts/src/AssetProxyOwner.sol
Normal file
@@ -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 "./MultiSigWalletWithTimeLock.sol";
|
||||
import "@0x/contracts-utils/contracts/src/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;
|
||||
}
|
||||
}
|
||||
}
|
58
contracts/multisig/contracts/test/TestAssetProxyOwner.sol
Normal file
58
contracts/multisig/contracts/test/TestAssetProxyOwner.sol
Normal file
@@ -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 "../src/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;
|
||||
}
|
||||
}
|
@@ -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",
|
||||
@@ -48,13 +48,9 @@
|
||||
"@0x/contracts-test-utils": "^2.0.1",
|
||||
"@0x/dev-utils": "^1.0.24",
|
||||
"@0x/sol-compiler": "^2.0.2",
|
||||
"@0x/subproviders": "^2.1.11",
|
||||
"@0x/tslint-config": "^2.0.2",
|
||||
"@types/bn.js": "^4.11.0",
|
||||
"@types/ethereumjs-abi": "^0.6.0",
|
||||
"@types/lodash": "4.14.104",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^10.0.0",
|
||||
"chai": "^4.0.1",
|
||||
"chai-as-promised": "^7.1.0",
|
||||
"chai-bignumber": "^3.0.0",
|
||||
@@ -65,12 +61,12 @@
|
||||
"shx": "^0.2.2",
|
||||
"solhint": "^1.4.1",
|
||||
"tslint": "5.11.0",
|
||||
"typescript": "3.0.1",
|
||||
"yargs": "^10.0.3"
|
||||
"typescript": "3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@0x/base-contract": "^3.0.13",
|
||||
"@0x/order-utils": "^3.1.2",
|
||||
"@0x/contracts-asset-proxy": "1.0.0",
|
||||
"@0x/contracts-erc20": "1.0.0",
|
||||
"@0x/types": "^1.5.2",
|
||||
"@0x/typescript-typings": "^3.0.8",
|
||||
"@0x/utils": "^3.0.1",
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -1,2 +1,3 @@
|
||||
export * from './artifacts';
|
||||
export * from './wrappers';
|
||||
export * from '../test/utils';
|
||||
|
@@ -1,2 +1,5 @@
|
||||
export * from '../../generated-wrappers/asset_proxy_owner';
|
||||
export * from '../../generated-wrappers/multi_sig_wallet';
|
||||
export * from '../../generated-wrappers/multi_sig_wallet_with_time_lock';
|
||||
export * from '../../generated-wrappers/test_asset_proxy_owner';
|
||||
export * from '../../generated-wrappers/test_reject_ether';
|
||||
|
506
contracts/multisig/test/asset_proxy_owner.ts
Normal file
506
contracts/multisig/test/asset_proxy_owner.ts
Normal file
@@ -0,0 +1,506 @@
|
||||
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 {
|
||||
artifacts,
|
||||
AssetProxyOwnerAssetProxyRegistrationEventArgs,
|
||||
AssetProxyOwnerContract,
|
||||
AssetProxyOwnerExecutionEventArgs,
|
||||
AssetProxyOwnerExecutionFailureEventArgs,
|
||||
AssetProxyOwnerSubmissionEventArgs,
|
||||
AssetProxyOwnerWrapper,
|
||||
TestAssetProxyOwnerContract,
|
||||
} from '../src';
|
||||
|
||||
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
|
@@ -16,17 +16,16 @@ import { LogWithDecodedArgs } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import {
|
||||
artifacts,
|
||||
MultiSigWalletWithTimeLockConfirmationEventArgs,
|
||||
MultiSigWalletWithTimeLockConfirmationTimeSetEventArgs,
|
||||
MultiSigWalletWithTimeLockContract,
|
||||
MultiSigWalletWithTimeLockExecutionEventArgs,
|
||||
MultiSigWalletWithTimeLockExecutionFailureEventArgs,
|
||||
MultiSigWalletWithTimeLockSubmissionEventArgs,
|
||||
} from '../generated-wrappers/multi_sig_wallet_with_time_lock';
|
||||
import { TestRejectEtherContract } from '../generated-wrappers/test_reject_ether';
|
||||
import { artifacts } from '../src/artifacts';
|
||||
|
||||
import { MultiSigWrapper } from './utils/multi_sig_wrapper';
|
||||
MultiSigWrapper,
|
||||
TestRejectEtherContract,
|
||||
} from '../src';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
71
contracts/multisig/test/utils/asset_proxy_owner_wrapper.ts
Normal file
71
contracts/multisig/test/utils/asset_proxy_owner_wrapper.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { artifacts as proxyArtifacts } from '@0x/contracts-asset-proxy';
|
||||
import { artifacts as erc20Artifacts } from '@0x/contracts-erc20';
|
||||
import { LogDecoder } from '@0x/contracts-test-utils';
|
||||
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, ...erc20Artifacts, ...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;
|
||||
}
|
||||
}
|
2
contracts/multisig/test/utils/index.ts
Normal file
2
contracts/multisig/test/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './asset_proxy_owner_wrapper';
|
||||
export * from './multi_sig_wrapper';
|
@@ -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"]
|
||||
|
Reference in New Issue
Block a user