import { BaseContract } from '@0x/base-contract'; import { blockchainTests, constants, expect, randomAddress } from '@0x/contracts-test-utils'; import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils'; import { DataItem, MethodAbi } from 'ethereum-types'; import * as _ from 'lodash'; import { artifacts } from './artifacts'; import { abis } from './utils/abis'; import { deployFullFeaturesAsync, FullFeatures } from './utils/migration'; import { IMetaTransactionsFeatureContract, INativeOrdersFeatureContract, IOwnableFeatureContract, ITransformERC20FeatureContract, TestFullMigrationContract, ZeroExContract, } from './wrappers'; const { NULL_ADDRESS } = constants; blockchainTests.resets('Full migration', env => { let owner: string; let zeroEx: ZeroExContract; let features: FullFeatures; let migrator: TestFullMigrationContract; const transformerDeployer = randomAddress(); before(async () => { [owner] = await env.getAccountAddressesAsync(); migrator = await TestFullMigrationContract.deployFrom0xArtifactAsync( artifacts.TestFullMigration, env.provider, env.txDefaults, artifacts, env.txDefaults.from as string, ); zeroEx = await ZeroExContract.deployFrom0xArtifactAsync( artifacts.ZeroEx, env.provider, env.txDefaults, artifacts, await migrator.getBootstrapper().callAsync(), ); features = await deployFullFeaturesAsync(env.provider, env.txDefaults, { zeroExAddress: zeroEx.address }); await migrator .migrateZeroEx(owner, zeroEx.address, features, { transformerDeployer }) .awaitTransactionSuccessAsync(); }); it('ZeroEx has the correct owner', async () => { const ownable = new IOwnableFeatureContract(zeroEx.address, env.provider, env.txDefaults); const actualOwner = await ownable.owner().callAsync(); expect(actualOwner).to.eq(owner); }); it('FullMigration contract self-destructs', async () => { const dieRecipient = await migrator.dieRecipient().callAsync(); expect(dieRecipient).to.eq(owner); }); it('Non-deployer cannot call migrateZeroEx()', async () => { const notDeployer = randomAddress(); const tx = migrator .migrateZeroEx(owner, zeroEx.address, features, { transformerDeployer }) .callAsync({ from: notDeployer }); return expect(tx).to.revertWith('FullMigration/INVALID_SENDER'); }); const FEATURE_FNS = { TransformERC20: { contractType: ITransformERC20FeatureContract, fns: [ 'transformERC20', '_transformERC20', 'createTransformWallet', 'getTransformWallet', 'setTransformerDeployer', 'getQuoteSigner', 'setQuoteSigner', ], }, MetaTransactions: { contractType: IMetaTransactionsFeatureContract, fns: [ 'executeMetaTransaction', 'batchExecuteMetaTransactions', 'getMetaTransactionExecutedBlock', 'getMetaTransactionHashExecutedBlock', 'getMetaTransactionHash', ], }, NativeOrdersFeature: { contractType: INativeOrdersFeatureContract, fns: [ 'transferProtocolFeesForPools', 'fillLimitOrder', 'fillRfqOrder', 'fillOrKillLimitOrder', 'fillOrKillRfqOrder', '_fillLimitOrder', '_fillRfqOrder', 'cancelLimitOrder', 'cancelRfqOrder', 'batchCancelLimitOrders', 'batchCancelRfqOrders', 'cancelPairLimitOrders', 'batchCancelPairLimitOrders', 'cancelPairRfqOrders', 'batchCancelPairRfqOrders', 'getLimitOrderInfo', 'getRfqOrderInfo', 'getLimitOrderHash', 'getRfqOrderHash', 'getProtocolFeeMultiplier', 'registerAllowedRfqOrigins', 'getLimitOrderRelevantState', 'getRfqOrderRelevantState', 'batchGetLimitOrderRelevantStates', 'batchGetRfqOrderRelevantStates', ], }, }; function createFakeInputs(inputs: DataItem[] | DataItem): any | any[] { if ((inputs as DataItem[]).length !== undefined) { return (inputs as DataItem[]).map(i => createFakeInputs(i)); } const item = inputs as DataItem; // TODO(dorothy-zbornak): Support fixed-length arrays. if (/\[]$/.test(item.type)) { return _.times(_.random(0, 8), () => createFakeInputs({ ...item, type: item.type.substring(0, item.type.length - 2), }), ); } if (/^tuple$/.test(item.type)) { const tuple = {} as any; for (const comp of item.components as DataItem[]) { tuple[comp.name] = createFakeInputs(comp); } return tuple; } if (item.type === 'address') { return randomAddress(); } if (item.type === 'byte') { return hexUtils.random(1); } if (/^bytes$/.test(item.type)) { return hexUtils.random(_.random(0, 128)); } if (/^bytes\d+$/.test(item.type)) { return hexUtils.random(parseInt(/\d+$/.exec(item.type)![0], 10)); } if (/^uint\d+$/.test(item.type)) { if (item.type === 'uint8') { // Solidity will revert if enum values are out of range, so // play it safe and pick zero. return 0; } return new BigNumber(hexUtils.random(parseInt(/\d+$/.exec(item.type)![0], 10) / 8)); } if (/^int\d+$/.test(item.type)) { return new BigNumber(hexUtils.random(parseInt(/\d+$/.exec(item.type)![0], 10) / 8)) .div(2) .times(_.sample([-1, 1])!); } throw new Error(`Unhandled input type: '${item.type}'`); } for (const [featureName, featureInfo] of Object.entries(FEATURE_FNS)) { describe(`${featureName} feature`, () => { let contract: BaseContract & { getSelector(name: string): string }; before(async () => { contract = new featureInfo.contractType(zeroEx.address, env.provider, env.txDefaults, abis); }); for (const fn of featureInfo.fns) { it(`${fn} is registered`, async () => { const selector = contract.getSelector(fn); const impl = await zeroEx.getFunctionImplementation(selector).callAsync(); expect(impl).to.not.eq(NULL_ADDRESS); }); if (fn.startsWith('_')) { it(`${fn} cannot be called from outside`, async () => { const method = contract.abi.find( d => d.type === 'function' && (d as MethodAbi).name === fn, ) as MethodAbi; const inputs = createFakeInputs(method.inputs); const tx = (contract as any)[fn](...inputs).callAsync(); return expect(tx).to.revertWith( new ZeroExRevertErrors.Common.OnlyCallableBySelfError(env.txDefaults.from), ); }); } } }); } describe('TransformERC20', () => { let feature: ITransformERC20FeatureContract; before(async () => { feature = new ITransformERC20FeatureContract(zeroEx.address, env.provider, env.txDefaults); }); it('has the correct transformer deployer', async () => { return expect(feature.getTransformerDeployer().callAsync()).to.become(transformerDeployer); }); }); });