import { BlockchainLifecycle } from '@0xproject/dev-utils';
import { RevertReason } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import { LogWithDecodedArgs } from 'ethereum-types';
import * as _ from 'lodash';
import { DummyERC20TokenContract } from '../../generated_contract_wrappers/dummy_erc20_token';
import {
MultiSigWalletWithTimeLockConfirmationEventArgs,
MultiSigWalletWithTimeLockConfirmationTimeSetEventArgs,
MultiSigWalletWithTimeLockContract,
MultiSigWalletWithTimeLockExecutionEventArgs,
MultiSigWalletWithTimeLockExecutionFailureEventArgs,
MultiSigWalletWithTimeLockSubmissionEventArgs,
} from '../../generated_contract_wrappers/multi_sig_wallet_with_time_lock';
import { artifacts } from '../utils/artifacts';
import { expectTransactionFailedAsync, expectTransactionFailedWithoutReasonAsync } from '../utils/assertions';
import { increaseTimeAndMineBlockAsync } from '../utils/block_timestamp';
import { chaiSetup } from '../utils/chai_setup';
import { constants } from '../utils/constants';
import { MultiSigWrapper } from '../utils/multi_sig_wrapper';
import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
// tslint:disable:no-unnecessary-type-assertion
describe('MultiSigWalletWithTimeLock', () => {
let owners: string[];
let notOwner: string;
const REQUIRED_APPROVALS = new BigNumber(2);
const SECONDS_TIME_LOCKED = new BigNumber(1000000);
before(async () => {
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
before(async () => {
const accounts = await web3Wrapper.getAvailableAddressesAsync();
owners = [accounts[0], accounts[1], accounts[2]];
notOwner = accounts[3];
});
let multiSig: MultiSigWalletWithTimeLockContract;
let multiSigWrapper: MultiSigWrapper;
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('external_call', () => {
it('should be internal', async () => {
const secondsTimeLocked = new BigNumber(0);
multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync(
artifacts.MultiSigWalletWithTimeLock,
provider,
txDefaults,
owners,
REQUIRED_APPROVALS,
secondsTimeLocked,
);
expect(_.isUndefined((multiSig as any).external_call)).to.be.equal(true);
});
});
describe('confirmTransaction', () => {
let txId: BigNumber;
beforeEach(async () => {
const secondsTimeLocked = new BigNumber(0);
multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync(
artifacts.MultiSigWalletWithTimeLock,
provider,
txDefaults,
owners,
REQUIRED_APPROVALS,
secondsTimeLocked,
);
multiSigWrapper = new MultiSigWrapper(multiSig, provider);
const destination = notOwner;
const data = constants.NULL_BYTES;
const txReceipt = await multiSigWrapper.submitTransactionAsync(destination, data, owners[0]);
txId = (txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>).args
.transactionId;
});
it('should revert if called by a non-owner', async () => {
await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.confirmTransactionAsync(txId, notOwner));
});
it('should revert if transaction does not exist', async () => {
const nonexistentTxId = new BigNumber(123456789);
await expectTransactionFailedWithoutReasonAsync(
multiSigWrapper.confirmTransactionAsync(nonexistentTxId, owners[1]),
);
});
it('should revert if transaction is already confirmed by caller', async () => {
await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.confirmTransactionAsync(txId, owners[0]));
});
it('should confirm transaction for caller and log a Confirmation event', async () => {
const txReceipt = await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
const log = txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockConfirmationEventArgs>;
expect(log.event).to.be.equal('Confirmation');
expect(log.args.sender).to.be.equal(owners[1]);
expect(log.args.transactionId).to.be.bignumber.equal(txId);
});
it('should revert if fully confirmed', async () => {
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
await expectTransactionFailedAsync(
multiSigWrapper.confirmTransactionAsync(txId, owners[2]),
RevertReason.TxFullyConfirmed,
);
});
it('should set the confirmation time of the transaction if it becomes fully confirmed', async () => {
const blockNum = await web3Wrapper.getBlockNumberAsync();
const timestamp = new BigNumber(await web3Wrapper.getBlockTimestampAsync(blockNum));
const txReceipt = await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
const log = txReceipt.logs[1] as LogWithDecodedArgs<MultiSigWalletWithTimeLockConfirmationTimeSetEventArgs>;
expect(log.args.confirmationTime).to.be.bignumber.equal(timestamp);
expect(log.args.transactionId).to.be.bignumber.equal(txId);
});
});
describe('executeTransaction', () => {
let txId: BigNumber;
const secondsTimeLocked = new BigNumber(1000000);
beforeEach(async () => {
multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync(
artifacts.MultiSigWalletWithTimeLock,
provider,
txDefaults,
owners,
REQUIRED_APPROVALS,
secondsTimeLocked,
);
multiSigWrapper = new MultiSigWrapper(multiSig, provider);
const destination = notOwner;
const data = constants.NULL_BYTES;
const txReceipt = await multiSigWrapper.submitTransactionAsync(destination, data, owners[0]);
txId = (txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>).args
.transactionId;
});
it('should revert if transaction has not been fully confirmed', async () => {
await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
await expectTransactionFailedAsync(
multiSigWrapper.executeTransactionAsync(txId, owners[1]),
RevertReason.TxNotFullyConfirmed,
);
});
it('should revert if time lock has not passed', async () => {
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
await expectTransactionFailedAsync(
multiSigWrapper.executeTransactionAsync(txId, owners[1]),
RevertReason.TimeLockIncomplete,
);
});
it('should execute a transaction and log an Execution event if successful and called by owner', async () => {
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, owners[1]);
const log = txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockExecutionEventArgs>;
expect(log.event).to.be.equal('Execution');
expect(log.args.transactionId).to.be.bignumber.equal(txId);
});
it('should execute a transaction and log an Execution event if successful and called by non-owner', async () => {
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, notOwner);
const log = txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockExecutionEventArgs>;
expect(log.event).to.be.equal('Execution');
expect(log.args.transactionId).to.be.bignumber.equal(txId);
});
it('should revert if a required confirmation is revoked before executeTransaction is called', async () => {
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
await multiSigWrapper.revokeConfirmationAsync(txId, owners[0]);
await expectTransactionFailedAsync(
multiSigWrapper.executeTransactionAsync(txId, owners[1]),
RevertReason.TxNotFullyConfirmed,
);
});
it('should revert if transaction has been executed', async () => {
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
const txReceipt = await multiSigWrapper.executeTransactionAsync(txId, owners[1]);
const log = txReceipt.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockExecutionEventArgs>;
expect(log.args.transactionId).to.be.bignumber.equal(txId);
await expectTransactionFailedWithoutReasonAsync(multiSigWrapper.executeTransactionAsync(txId, owners[1]));
});
it("should log an ExecutionFailure event and not update the transaction's execution state if unsuccessful", async () => {
const contractWithoutFallback = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.DummyERC20Token,
provider,
txDefaults,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
constants.DUMMY_TOKEN_DECIMALS,
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
const data = constants.NULL_BYTES;
const value = new BigNumber(10);
const submissionTxReceipt = await multiSigWrapper.submitTransactionAsync(
contractWithoutFallback.address,
data,
owners[0],
{ value },
);
const newTxId = (submissionTxReceipt.logs[0] as LogWithDecodedArgs<
MultiSigWalletWithTimeLockSubmissionEventArgs
>).args.transactionId;
await multiSigWrapper.confirmTransactionAsync(newTxId, owners[1]);
await increaseTimeAndMineBlockAsync(secondsTimeLocked.toNumber());
const txReceipt = await multiSigWrapper.executeTransactionAsync(newTxId, owners[1]);
const executionFailureLog = txReceipt.logs[0] as LogWithDecodedArgs<
MultiSigWalletWithTimeLockExecutionFailureEventArgs
>;
expect(executionFailureLog.event).to.be.equal('ExecutionFailure');
expect(executionFailureLog.args.transactionId).to.be.bignumber.equal(newTxId);
});
});
describe('changeTimeLock', () => {
describe('initially non-time-locked', async () => {
before(async () => {
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
before('deploy a wallet', async () => {
const secondsTimeLocked = new BigNumber(0);
multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync(
artifacts.MultiSigWalletWithTimeLock,
provider,
txDefaults,
owners,
REQUIRED_APPROVALS,
secondsTimeLocked,
);
multiSigWrapper = new MultiSigWrapper(multiSig, provider);
});
it('should throw when not called by wallet', async () => {
return expectTransactionFailedWithoutReasonAsync(
multiSig.changeTimeLock.sendTransactionAsync(SECONDS_TIME_LOCKED, { from: owners[0] }),
);
});
it('should throw without enough confirmations', async () => {
const destination = multiSig.address;
const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED);
const res = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]);
const log = res.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>;
const txId = log.args.transactionId;
return expectTransactionFailedAsync(
multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }),
RevertReason.TxNotFullyConfirmed,
);
});
it('should set confirmation time with enough confirmations', async () => {
const destination = multiSig.address;
const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED);
const subRes = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]);
const subLog = subRes.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>;
const txId = subLog.args.transactionId;
const confirmRes = await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
expect(confirmRes.logs).to.have.length(2);
const blockNum = await web3Wrapper.getBlockNumberAsync();
const blockInfo = await web3Wrapper.getBlockAsync(blockNum);
const timestamp = new BigNumber(blockInfo.timestamp);
const confirmationTimeBigNum = new BigNumber(await multiSig.confirmationTimes.callAsync(txId));
expect(timestamp).to.be.bignumber.equal(confirmationTimeBigNum);
});
it('should be executable with enough confirmations and secondsTimeLocked of 0', async () => {
const destination = multiSig.address;
const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(SECONDS_TIME_LOCKED);
const subRes = await multiSigWrapper.submitTransactionAsync(destination, changeTimeLockData, owners[0]);
const subLog = subRes.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>;
const txId = subLog.args.transactionId;
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
await multiSigWrapper.executeTransactionAsync(txId, owners[1]);
const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync());
expect(secondsTimeLocked).to.be.bignumber.equal(SECONDS_TIME_LOCKED);
});
});
describe('initially time-locked', async () => {
before(async () => {
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
let txId: BigNumber;
const newSecondsTimeLocked = new BigNumber(0);
before('deploy a wallet, submit transaction to change timelock, and confirm the transaction', async () => {
multiSig = await MultiSigWalletWithTimeLockContract.deployFrom0xArtifactAsync(
artifacts.MultiSigWalletWithTimeLock,
provider,
txDefaults,
owners,
REQUIRED_APPROVALS,
SECONDS_TIME_LOCKED,
);
multiSigWrapper = new MultiSigWrapper(multiSig, provider);
const changeTimeLockData = multiSig.changeTimeLock.getABIEncodedTransactionData(newSecondsTimeLocked);
const res = await multiSigWrapper.submitTransactionAsync(
multiSig.address,
changeTimeLockData,
owners[0],
);
const log = res.logs[0] as LogWithDecodedArgs<MultiSigWalletWithTimeLockSubmissionEventArgs>;
txId = log.args.transactionId;
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
});
it('should throw if it has enough confirmations but is not past the time lock', async () => {
return expectTransactionFailedAsync(
multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }),
RevertReason.TimeLockIncomplete,
);
});
it('should execute if it has enough confirmations and is past the time lock', async () => {
await increaseTimeAndMineBlockAsync(SECONDS_TIME_LOCKED.toNumber());
await web3Wrapper.awaitTransactionSuccessAsync(
await multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] }),
constants.AWAIT_TRANSACTION_MINED_MS,
);
const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync());
expect(secondsTimeLocked).to.be.bignumber.equal(newSecondsTimeLocked);
});
});
});
});
// tslint:enable:no-unnecessary-type-assertion