diff --git a/contracts/integrations/CHANGELOG.json b/contracts/integrations/CHANGELOG.json index fbb64a370c..719fba04c3 100644 --- a/contracts/integrations/CHANGELOG.json +++ b/contracts/integrations/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Add aggregator mainnet tests.", "pr": 2407 + }, + { + "note": "Add fuzz tests for Exchange signature validation.", + "pr": 2425 } ], "timestamp": 1578272714 diff --git a/contracts/integrations/contracts/test/TestSignatureValidationWallet.sol b/contracts/integrations/contracts/test/TestSignatureValidationWallet.sol new file mode 100644 index 0000000000..3600c9a0ac --- /dev/null +++ b/contracts/integrations/contracts/test/TestSignatureValidationWallet.sol @@ -0,0 +1,53 @@ +/* + + Copyright 2019 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.5.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/LibEIP1271.sol"; + + +contract TestSignatureValidationWallet is + LibEIP1271 +{ + bytes4 private constant LEGACY_WALLET_MAGIC_VALUE = 0xb0671381; + + // Callback used by `EIP1271Wallet` and `Validator` signature types. + function isValidSignature( + bytes memory, + bytes memory + ) + public + pure + returns (bytes4 magicValue) + { + return EIP1271_MAGIC_VALUE; + } + + // Callback used by `Wallet` signature type. + function isValidSignature( + bytes32, + bytes memory + ) + public + pure + returns (bytes4 magicValue) + { + return LEGACY_WALLET_MAGIC_VALUE; + } +} diff --git a/contracts/integrations/package.json b/contracts/integrations/package.json index 54adf4dea9..dd36a2fb3c 100644 --- a/contracts/integrations/package.json +++ b/contracts/integrations/package.json @@ -38,7 +38,7 @@ }, "config": { "publicInterfaceContracts": "TestFramework", - "abis": "./test/generated-artifacts/@(TestDydxUser|TestEth2Dai|TestEth2DaiBridge|TestFramework|TestMainnetAggregatorFills|TestUniswapBridge|TestUniswapExchange|TestUniswapExchangeFactory).json", + "abis": "./test/generated-artifacts/@(TestDydxUser|TestEth2Dai|TestEth2DaiBridge|TestFramework|TestMainnetAggregatorFills|TestSignatureValidationWallet|TestUniswapBridge|TestUniswapExchange|TestUniswapExchangeFactory).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/integrations/test/artifacts.ts b/contracts/integrations/test/artifacts.ts index 3673b475fe..e809cb0a55 100644 --- a/contracts/integrations/test/artifacts.ts +++ b/contracts/integrations/test/artifacts.ts @@ -10,6 +10,7 @@ import * as TestEth2Dai from '../test/generated-artifacts/TestEth2Dai.json'; import * as TestEth2DaiBridge from '../test/generated-artifacts/TestEth2DaiBridge.json'; import * as TestFramework from '../test/generated-artifacts/TestFramework.json'; import * as TestMainnetAggregatorFills from '../test/generated-artifacts/TestMainnetAggregatorFills.json'; +import * as TestSignatureValidationWallet from '../test/generated-artifacts/TestSignatureValidationWallet.json'; import * as TestUniswapBridge from '../test/generated-artifacts/TestUniswapBridge.json'; import * as TestUniswapExchange from '../test/generated-artifacts/TestUniswapExchange.json'; import * as TestUniswapExchangeFactory from '../test/generated-artifacts/TestUniswapExchangeFactory.json'; @@ -19,6 +20,7 @@ export const artifacts = { TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, TestFramework: TestFramework as ContractArtifact, TestMainnetAggregatorFills: TestMainnetAggregatorFills as ContractArtifact, + TestSignatureValidationWallet: TestSignatureValidationWallet as ContractArtifact, TestUniswapBridge: TestUniswapBridge as ContractArtifact, TestUniswapExchange: TestUniswapExchange as ContractArtifact, TestUniswapExchangeFactory: TestUniswapExchangeFactory as ContractArtifact, diff --git a/contracts/integrations/test/framework/deployment_manager.ts b/contracts/integrations/test/framework/deployment_manager.ts index a2e32a165e..ac5f5e0b27 100644 --- a/contracts/integrations/test/framework/deployment_manager.ts +++ b/contracts/integrations/test/framework/deployment_manager.ts @@ -22,6 +22,7 @@ import { } from '@0x/contracts-staking'; import { BlockchainTestsEnvironment, constants } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; import { TxData } from 'ethereum-types'; import * as _ from 'lodash'; @@ -207,6 +208,7 @@ export class DeploymentManager { // Construct the new instance and return it. return new DeploymentManager( + environment.web3Wrapper, assetProxies, governor, exchange, @@ -522,6 +524,7 @@ export class DeploymentManager { } protected constructor( + public web3Wrapper: Web3Wrapper, public assetProxies: AssetProxyContracts, public governor: ZeroExGovernorContract, public exchange: ExchangeContract, diff --git a/contracts/integrations/test/framework/simulation.ts b/contracts/integrations/test/framework/simulation.ts index 85e554a761..807f397b8a 100644 --- a/contracts/integrations/test/framework/simulation.ts +++ b/contracts/integrations/test/framework/simulation.ts @@ -48,6 +48,7 @@ export class SimulationEnvironment { export abstract class Simulation { public readonly generator = this._assertionGenerator(); + public resets = false; constructor(public environment: SimulationEnvironment) {} @@ -66,11 +67,15 @@ export abstract class Simulation { protected abstract _assertionGenerator(): AsyncIterableIterator; private async _stepAsync(): Promise { + const snapshotId = this.resets ? await this.environment.deployment.web3Wrapper.takeSnapshotAsync() : undefined; try { await this.generator.next(); } catch (error) { logger.logFailure(error, this.environment.state()); throw error; } + if (snapshotId !== undefined) { + await this.environment.deployment.web3Wrapper.revertSnapshotAsync(snapshotId); + } } } diff --git a/contracts/integrations/test/framework/utils/pseudorandom.ts b/contracts/integrations/test/framework/utils/pseudorandom.ts index c39609863f..59863104b3 100644 --- a/contracts/integrations/test/framework/utils/pseudorandom.ts +++ b/contracts/integrations/test/framework/utils/pseudorandom.ts @@ -78,6 +78,17 @@ class PRNGWrapper { return ONE.minus(ONE.minus(u).exponentiatedBy(ONE.dividedBy(beta))).exponentiatedBy(ONE.dividedBy(alpha)); }; } + + /* + * Pseudorandom version of `hexRandom()`. If no distribution is provided, + * samples all byte values uniformly. + */ + public hex(bytesLength: number = 32, distribution: () => Numberish = this.rng): string { + const buf = Buffer.from(_.times(bytesLength, () => this.integer(0, 255, distribution).toNumber())).toString( + 'hex', + ); + return `0x${buf}`; + } } export const Pseudorandom = new PRNGWrapper(); diff --git a/contracts/integrations/test/fuzz_tests/exchange_signature_validation_test.ts b/contracts/integrations/test/fuzz_tests/exchange_signature_validation_test.ts new file mode 100644 index 0000000000..41c1cdfb3c --- /dev/null +++ b/contracts/integrations/test/fuzz_tests/exchange_signature_validation_test.ts @@ -0,0 +1,725 @@ +import { ExchangeContract } from '@0x/contracts-exchange'; +import { blockchainTests, constants, expect, signingUtils, transactionHashUtils } from '@0x/contracts-test-utils'; +import { orderHashUtils } from '@0x/order-utils'; +import { Order, SignatureType, ZeroExTransaction } from '@0x/types'; +import { hexUtils, logUtils } from '@0x/utils'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { artifacts } from '../artifacts'; +import { AssertionResult } from '../framework/assertions/function_assertion'; +import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store'; +import { DeploymentManager } from '../framework/deployment_manager'; +import { Simulation, SimulationEnvironment } from '../framework/simulation'; +import { Pseudorandom } from '../framework/utils/pseudorandom'; +import { TestSignatureValidationWalletContract } from '../wrappers'; + +// tslint:disable: max-classes-per-file no-non-null-assertion no-unnecessary-type-assertion +const tests = process.env.FUZZ_TEST === 'exchange/signature_validation' ? blockchainTests : blockchainTests.skip; + +tests('Exchange signature validation fuzz tests', env => { + const ALL_SIGNATURE_TYPES = [ + SignatureType.Illegal, + SignatureType.Invalid, + SignatureType.EthSign, + SignatureType.EIP712, + SignatureType.Wallet, + SignatureType.Validator, + SignatureType.PreSigned, + SignatureType.EIP1271Wallet, + ]; + const ALL_WORKING_SIGNATURE_TYPES = [ + SignatureType.EthSign, + SignatureType.EIP712, + SignatureType.Wallet, + SignatureType.Validator, + SignatureType.PreSigned, + SignatureType.EIP1271Wallet, + ]; + const HASH_COMPATIBLE_SIGNATURE_TYPES = [ + SignatureType.EthSign, + SignatureType.EIP712, + SignatureType.Wallet, + SignatureType.PreSigned, + ]; + const STATIC_SIGNATURE_TYPES = [SignatureType.EthSign, SignatureType.EIP712, SignatureType.PreSigned]; + const ALWAYS_FAILING_SIGNATURE_TYPES = [SignatureType.Illegal, SignatureType.Invalid]; + const WALLET_SIGNATURE_TYPES = [SignatureType.Wallet, SignatureType.EIP1271Wallet]; + const STRICT_LENGTH_SIGNATURE_TYPES = [SignatureType.EthSign, SignatureType.EIP712]; + const CALLBACK_SIGNATURE_TYPES = [SignatureType.Wallet, SignatureType.EIP1271Wallet, SignatureType.Validator]; + + let walletContractAddress: string; + let notWalletContractAddress: string; + let deployment: DeploymentManager; + let exchange: ExchangeContract; + let accounts: string[]; + let privateKeys: { [address: string]: Buffer }; + let chainId: number; + + interface SignatureTestParams { + signatureType: SignatureType; + signer: string; + signature: string; + hash: string; + signerKey?: Buffer; + validator?: string; + payload?: string; + order?: Order; + transaction?: ZeroExTransaction; + } + + before(async () => { + chainId = await env.web3Wrapper.getChainIdAsync(); + accounts = await env.getAccountAddressesAsync(); + privateKeys = _.zipObject(accounts, accounts.map((a, i) => constants.TESTRPC_PRIVATE_KEYS[i])); + deployment = await DeploymentManager.deployAsync(env, { + numErc20TokensToDeploy: 0, + numErc721TokensToDeploy: 0, + numErc1155TokensToDeploy: 0, + }); + exchange = deployment.exchange; + walletContractAddress = (await TestSignatureValidationWalletContract.deployFrom0xArtifactAsync( + artifacts.TestSignatureValidationWallet, + env.provider, + env.txDefaults, + {}, + )).address; + // This just has to be a contract address that doesn't implement the + // wallet spec. + notWalletContractAddress = exchange.address; + }); + + function randomPayload(): string { + return Pseudorandom.hex(Pseudorandom.integer(0, 66).toNumber()); + } + + async function presignHashAsync(signer: string, hash: string): Promise { + await exchange.preSign(hash).awaitTransactionSuccessAsync({ from: signer }); + } + + async function approveValidatorAsync(signer: string, validator: string, approved: boolean = true): Promise { + await exchange + .setSignatureValidatorApproval(validator, approved) + .awaitTransactionSuccessAsync({ from: signer }); + } + + function createSignature(params: { + signatureType: SignatureType; + hash?: string; + signerKey?: Buffer; + validator?: string; + payload?: string; + }): string { + const payload = params.payload || constants.NULL_BYTES; + const signatureByte = hexUtils.leftPad(params.signatureType, 1); + switch (params.signatureType) { + default: + case SignatureType.Illegal: + case SignatureType.Invalid: + case SignatureType.PreSigned: + return hexUtils.concat(payload, signatureByte); + case SignatureType.EIP712: + case SignatureType.EthSign: + return hexUtils.concat( + payload, + ethUtil.bufferToHex( + signingUtils.signMessage( + ethUtil.toBuffer(params.hash), + params.signerKey!, + params.signatureType, + ), + ), + ); + case SignatureType.Wallet: + case SignatureType.EIP1271Wallet: + return hexUtils.concat(payload, params.signatureType); + case SignatureType.Validator: + return hexUtils.concat(payload, params.validator!, params.signatureType); + } + } + + async function mangleSignatureParamsAsync(params: SignatureTestParams): Promise { + const mangled = { ...params }; + const MANGLE_MODES = [ + 'TRUNCATE_SIGNATURE', + 'RETYPE_SIGNATURE', + 'RANDOM_HASH', + 'RANDOM_ORDER', + 'RANDOM_TRANSACTION', + 'RANDOM_SIGNER', + ]; + const invalidModes = []; + if (!STRICT_LENGTH_SIGNATURE_TYPES.includes(mangled.signatureType)) { + invalidModes.push('TRUNCATE_SIGNATURE'); + } + if (CALLBACK_SIGNATURE_TYPES.includes(mangled.signatureType)) { + invalidModes.push('RANDOM_HASH'); + } + if (params.transaction === undefined) { + invalidModes.push('RANDOM_TRANSACTION'); + } + if (params.order === undefined) { + invalidModes.push('RANDOM_ORDER'); + } + if (params.order !== undefined || params.hash !== undefined) { + invalidModes.push('RANDOM_HASH'); + } + const mode = Pseudorandom.sample(_.without(MANGLE_MODES, ...invalidModes))!; + switch (mode) { + case 'TRUNCATE_SIGNATURE': + while (hexUtils.slice(mangled.signature, -1) === hexUtils.leftPad(mangled.signatureType, 1)) { + mangled.signature = hexUtils.slice(mangled.signature, 0, -1); + } + break; + case 'RETYPE_SIGNATURE': + mangled.signatureType = WALLET_SIGNATURE_TYPES.includes(mangled.signatureType) + ? Pseudorandom.sample(_.without(ALL_SIGNATURE_TYPES, ...WALLET_SIGNATURE_TYPES))! + : Pseudorandom.sample(_.without(ALL_SIGNATURE_TYPES, mangled.signatureType))!; + mangled.signature = hexUtils.concat(hexUtils.slice(mangled.signature, 0, -1), mangled.signatureType); + break; + case 'RANDOM_SIGNER': + mangled.signer = Pseudorandom.hex(constants.ADDRESS_LENGTH); + if (mangled.order) { + mangled.order.makerAddress = mangled.signer; + } + if (mangled.transaction) { + mangled.transaction.signerAddress = mangled.signer; + } + break; + case 'RANDOM_HASH': + mangled.hash = Pseudorandom.hex(); + break; + case 'RANDOM_ORDER': + mangled.order = randomOrder({ + exchangeAddress: mangled.order!.exchangeAddress, + chainId: mangled.order!.chainId, + }); + mangled.hash = await orderHashUtils.getOrderHashAsync(mangled.order); + break; + case 'RANDOM_TRANSACTION': + mangled.transaction = randomTransaction({ + domain: mangled.transaction!.domain, + }); + mangled.hash = await transactionHashUtils.getTransactionHashHex(mangled.transaction); + break; + default: + throw new Error(`Unhandled mangle mode: ${mode}`); + } + return mangled; + } + + function createHashTestParams(fields: Partial = {}): SignatureTestParams { + const signatureType = + fields.signatureType === undefined + ? Pseudorandom.sample(HASH_COMPATIBLE_SIGNATURE_TYPES)! + : fields.signatureType; + const signer = + fields.signer || + (WALLET_SIGNATURE_TYPES.includes(signatureType) ? walletContractAddress : Pseudorandom.sample(accounts)!); + const validator = + fields.validator || (signatureType === SignatureType.Validator ? walletContractAddress : undefined); + const signerKey = fields.signerKey || privateKeys[signer]; + const hash = fields.hash || Pseudorandom.hex(); + const payload = + fields.payload || + (STRICT_LENGTH_SIGNATURE_TYPES.includes(signatureType) ? constants.NULL_BYTES : randomPayload()); + const signature = createSignature({ signatureType, hash, signerKey, payload, validator }); + return { + hash, + payload, + signature, + signatureType, + signer, + signerKey, + validator, + }; + } + + async function assertValidHashSignatureAsync(params: { + hash: string; + signer: string; + signature: string; + isValid: boolean; + }): Promise { + try { + let result; + try { + result = await exchange.isValidHashSignature(params.hash, params.signer, params.signature).callAsync(); + } catch (err) { + if (params.isValid) { + throw err; + } + return; + } + expect(result).to.eq(!!params.isValid); + } catch (err) { + logUtils.warn(params); + throw err; + } + } + + async function* validTestHashSignature(): AsyncIterableIterator { + while (true) { + const { hash, signature, signatureType, signer } = createHashTestParams(); + yield (async () => { + if (signatureType === SignatureType.PreSigned) { + await presignHashAsync(signer, hash); + } + await assertValidHashSignatureAsync({ + hash, + signer, + signature, + isValid: true, + }); + })(); + } + } + + async function* invalidTestHashStaticSignature(): AsyncIterableIterator { + while (true) { + const randomSignerKey = ethUtil.toBuffer(Pseudorandom.hex()); + const signer = Pseudorandom.sample([notWalletContractAddress, walletContractAddress, ...accounts])!; + const { hash, signature } = createHashTestParams({ + signatureType: Pseudorandom.sample([...STATIC_SIGNATURE_TYPES, ...ALWAYS_FAILING_SIGNATURE_TYPES])!, + signer, + // Always sign with a random key. + signerKey: randomSignerKey, + }); + yield assertValidHashSignatureAsync({ + hash, + signer, + signature, + isValid: false, + }); + } + } + + async function* invalidTestHashWalletSignature(): AsyncIterableIterator { + while (true) { + const signer = Pseudorandom.sample([notWalletContractAddress, ...accounts])!; + const { hash, signature } = createHashTestParams({ + signatureType: SignatureType.Wallet, + signer, + }); + yield assertValidHashSignatureAsync({ + hash, + signer, + signature, + isValid: false, + }); + } + } + + async function* invalidTestHashValidatorSignature(): AsyncIterableIterator { + while (true) { + const isNotApproved = Pseudorandom.sample([true, false])!; + const signer = Pseudorandom.sample([...accounts])!; + const validator = isNotApproved + ? walletContractAddress + : Pseudorandom.sample([ + // All validator signatures are invalid for the hash test, so passing a valid + // wallet contract should still fail. + walletContractAddress, + notWalletContractAddress, + ...accounts, + ])!; + const { hash, signature } = createHashTestParams({ + signatureType: SignatureType.Validator, + validator, + }); + yield (async () => { + if (!isNotApproved) { + await approveValidatorAsync(signer, validator); + } + await assertValidHashSignatureAsync({ + hash, + signer, + signature, + isValid: false, + }); + })(); + } + } + + async function* invalidTestHashMangledSignature(): AsyncIterableIterator { + while (true) { + const params = createHashTestParams({ signatureType: Pseudorandom.sample(ALL_SIGNATURE_TYPES)! }); + const mangled = await mangleSignatureParamsAsync(params); + yield (async () => { + await assertValidHashSignatureAsync({ + hash: mangled.hash, + signer: mangled.signer, + signature: mangled.signature, + isValid: false, + }); + })(); + } + } + + function randomOrder(fields: Partial = {}): Order { + return { + chainId, + exchangeAddress: exchange.address, + expirationTimeSeconds: Pseudorandom.integer(1, 2 ** 32), + salt: Pseudorandom.integer(0, constants.MAX_UINT256), + makerAssetData: Pseudorandom.hex(36), + takerAssetData: Pseudorandom.hex(36), + makerFeeAssetData: Pseudorandom.hex(36), + takerFeeAssetData: Pseudorandom.hex(36), + makerAssetAmount: Pseudorandom.integer(0, 100e18), + takerAssetAmount: Pseudorandom.integer(0, 100e18), + makerFee: Pseudorandom.integer(0, 100e18), + takerFee: Pseudorandom.integer(0, 100e18), + feeRecipientAddress: Pseudorandom.hex(constants.ADDRESS_LENGTH), + makerAddress: Pseudorandom.hex(constants.ADDRESS_LENGTH), + takerAddress: Pseudorandom.hex(constants.ADDRESS_LENGTH), + senderAddress: Pseudorandom.hex(constants.ADDRESS_LENGTH), + ...fields, + }; + } + + async function createOrderTestParamsAsync( + fields: Partial = {}, + ): Promise { + const signatureType = + fields.signatureType === undefined + ? Pseudorandom.sample(ALL_WORKING_SIGNATURE_TYPES)! + : fields.signatureType; + const signer = + fields.signer || + (WALLET_SIGNATURE_TYPES.includes(signatureType) ? walletContractAddress : Pseudorandom.sample(accounts)!); + const validator = + fields.validator || (signatureType === SignatureType.Validator ? walletContractAddress : undefined); + const signerKey = fields.signerKey || privateKeys[signer]; + const order = fields.order || randomOrder({ makerAddress: signer }); + const hash = fields.hash || (await orderHashUtils.getOrderHashAsync(order)); + const payload = + fields.payload || + (STRICT_LENGTH_SIGNATURE_TYPES.includes(signatureType) ? constants.NULL_BYTES : randomPayload()); + const signature = createSignature({ signatureType, hash, signerKey, payload, validator }); + return { + hash, + order, + payload, + signature, + signatureType, + signer, + signerKey, + validator, + }; + } + + async function assertValidOrderSignatureAsync(params: { + order: Order; + signature: string; + isValid: boolean; + }): Promise { + try { + let result; + try { + result = await exchange.isValidOrderSignature(params.order, params.signature).callAsync(); + } catch (err) { + if (params.isValid) { + throw err; + } + return; + } + expect(result).to.eq(!!params.isValid); + } catch (err) { + logUtils.warn(params); + throw err; + } + } + + async function* validTestOrderSignature(): AsyncIterableIterator { + while (true) { + const { hash, order, signature, signatureType, signer, validator } = await createOrderTestParamsAsync(); + yield (async () => { + if (signatureType === SignatureType.PreSigned) { + await presignHashAsync(signer, hash); + } else if (signatureType === SignatureType.Validator) { + await approveValidatorAsync(signer, validator!); + } + await assertValidOrderSignatureAsync({ + order, + signature, + isValid: true, + }); + })(); + } + } + + async function* invalidTestOrderStaticSignature(): AsyncIterableIterator { + while (true) { + const randomSignerKey = ethUtil.toBuffer(Pseudorandom.hex()); + const signer = Pseudorandom.sample([notWalletContractAddress, walletContractAddress, ...accounts])!; + const { order, signature } = await createOrderTestParamsAsync({ + signatureType: Pseudorandom.sample([...STATIC_SIGNATURE_TYPES, ...ALWAYS_FAILING_SIGNATURE_TYPES])!, + signer, + // Always sign with a random key. + signerKey: randomSignerKey, + }); + yield assertValidOrderSignatureAsync({ + order, + signature, + isValid: false, + }); + } + } + + async function* invalidTestOrderWalletSignature(): AsyncIterableIterator { + while (true) { + const signer = Pseudorandom.sample([notWalletContractAddress, ...accounts])!; + const { order, signature } = await createOrderTestParamsAsync({ + signatureType: Pseudorandom.sample(WALLET_SIGNATURE_TYPES)!, + signer, + }); + yield assertValidOrderSignatureAsync({ + order, + signature, + isValid: false, + }); + } + } + + async function* invalidTestOrderValidatorSignature(): AsyncIterableIterator { + while (true) { + const isNotApproved = Pseudorandom.sample([true, false])!; + const signer = Pseudorandom.sample([...accounts])!; + const validator = isNotApproved + ? walletContractAddress + : Pseudorandom.sample([notWalletContractAddress, ...accounts])!; + const { order, signature } = await createOrderTestParamsAsync({ + signatureType: SignatureType.Validator, + validator, + }); + yield (async () => { + if (!isNotApproved) { + await approveValidatorAsync(signer, validator); + } + await assertValidOrderSignatureAsync({ + order, + signature, + isValid: false, + }); + })(); + } + } + + async function* invalidTestOrderMangledSignature(): AsyncIterableIterator { + while (true) { + const params = await createOrderTestParamsAsync({ + signatureType: Pseudorandom.sample(ALL_SIGNATURE_TYPES)!, + }); + const mangled = await mangleSignatureParamsAsync(params); + yield (async () => { + await assertValidOrderSignatureAsync({ + order: mangled.order!, + signature: mangled.signature, + isValid: false, + }); + })(); + } + } + + function randomTransaction(fields: Partial = {}): ZeroExTransaction { + return { + domain: { + chainId, + verifyingContract: exchange.address, + name: '0x Protocol', + version: '3.0.0', + }, + gasPrice: Pseudorandom.integer(1e9, 100e9), + expirationTimeSeconds: Pseudorandom.integer(1, 2 ** 32), + salt: Pseudorandom.integer(0, constants.MAX_UINT256), + signerAddress: Pseudorandom.hex(constants.ADDRESS_LENGTH), + data: Pseudorandom.hex(Pseudorandom.integer(4, 128).toNumber()), + ...fields, + }; + } + + async function createTransactionTestParamsAsync( + fields: Partial = {}, + ): Promise { + const signatureType = + fields.signatureType === undefined + ? Pseudorandom.sample(ALL_WORKING_SIGNATURE_TYPES)! + : fields.signatureType; + const signer = + fields.signer || + (WALLET_SIGNATURE_TYPES.includes(signatureType) ? walletContractAddress : Pseudorandom.sample(accounts)!); + const validator = + fields.validator || (signatureType === SignatureType.Validator ? walletContractAddress : undefined); + const signerKey = fields.signerKey || privateKeys[signer]; + const transaction = fields.transaction || randomTransaction({ signerAddress: signer }); + const hash = fields.hash || transactionHashUtils.getTransactionHashHex(transaction); + const payload = + fields.payload || + (STRICT_LENGTH_SIGNATURE_TYPES.includes(signatureType) ? constants.NULL_BYTES : randomPayload()); + const signature = createSignature({ signatureType, hash, signerKey, payload, validator }); + return { + hash, + transaction, + payload, + signature, + signatureType, + signer, + signerKey, + validator, + }; + } + + async function assertValidTransactionSignatureAsync(params: { + transaction: ZeroExTransaction; + signature: string; + isValid: boolean; + }): Promise { + try { + let result; + try { + result = await exchange.isValidTransactionSignature(params.transaction, params.signature).callAsync(); + } catch (err) { + if (params.isValid) { + throw err; + } + return; + } + expect(result).to.eq(!!params.isValid); + } catch (err) { + logUtils.warn(params); + throw err; + } + } + + async function* validTestTransactionSignature(): AsyncIterableIterator { + while (true) { + const { + hash, + transaction, + signature, + signatureType, + signer, + validator, + } = await createTransactionTestParamsAsync(); + yield (async () => { + if (signatureType === SignatureType.PreSigned) { + await presignHashAsync(signer, hash); + } else if (signatureType === SignatureType.Validator) { + await approveValidatorAsync(signer, validator!); + } + await assertValidTransactionSignatureAsync({ + transaction, + signature, + isValid: true, + }); + })(); + } + } + + async function* invalidTestTransactionStaticSignature(): AsyncIterableIterator { + while (true) { + const randomSignerKey = ethUtil.toBuffer(Pseudorandom.hex()); + const signer = Pseudorandom.sample([notWalletContractAddress, walletContractAddress, ...accounts])!; + const { transaction, signature } = await createTransactionTestParamsAsync({ + signatureType: Pseudorandom.sample([...STATIC_SIGNATURE_TYPES, ...ALWAYS_FAILING_SIGNATURE_TYPES])!, + signer, + // Always sign with a random key. + signerKey: randomSignerKey, + }); + yield assertValidTransactionSignatureAsync({ + transaction, + signature, + isValid: false, + }); + } + } + + async function* invalidTestTransactionWalletSignature(): AsyncIterableIterator { + while (true) { + const signer = Pseudorandom.sample([notWalletContractAddress, ...accounts])!; + const { transaction, signature } = await createTransactionTestParamsAsync({ + signatureType: Pseudorandom.sample(WALLET_SIGNATURE_TYPES)!, + signer, + }); + yield assertValidTransactionSignatureAsync({ + transaction, + signature, + isValid: false, + }); + } + } + + async function* invalidTestTransactionValidatorSignature(): AsyncIterableIterator { + while (true) { + const isNotApproved = Pseudorandom.sample([true, false])!; + const signer = Pseudorandom.sample([...accounts])!; + const validator = isNotApproved + ? walletContractAddress + : Pseudorandom.sample([notWalletContractAddress, ...accounts])!; + const { transaction, signature } = await createTransactionTestParamsAsync({ + signatureType: SignatureType.Validator, + validator, + }); + yield (async () => { + if (!isNotApproved) { + await approveValidatorAsync(signer, validator); + } + await assertValidTransactionSignatureAsync({ + transaction, + signature, + isValid: false, + }); + })(); + } + } + + async function* invalidTestTransactionMangledSignature(): AsyncIterableIterator { + while (true) { + const params = await createTransactionTestParamsAsync({ + signatureType: Pseudorandom.sample(ALL_SIGNATURE_TYPES)!, + }); + const mangled = await mangleSignatureParamsAsync(params); + yield (async () => { + await assertValidTransactionSignatureAsync({ + transaction: mangled.transaction!, + signature: mangled.signature, + isValid: false, + }); + })(); + } + } + + it('fuzz', async () => { + const FUZZ_ACTIONS = [ + validTestHashSignature(), + invalidTestHashStaticSignature(), + invalidTestHashWalletSignature(), + invalidTestHashValidatorSignature(), + invalidTestHashMangledSignature(), + validTestOrderSignature(), + invalidTestOrderStaticSignature(), + invalidTestOrderWalletSignature(), + invalidTestOrderValidatorSignature(), + invalidTestOrderMangledSignature(), + validTestTransactionSignature(), + invalidTestTransactionStaticSignature(), + invalidTestTransactionWalletSignature(), + invalidTestTransactionValidatorSignature(), + invalidTestTransactionMangledSignature(), + ]; + const simulationEnvironment = new SimulationEnvironment(deployment, new BlockchainBalanceStore({}, {}), []); + const simulation = new class extends Simulation { + // tslint:disable-next-line: prefer-function-over-method + protected async *_assertionGenerator(): AsyncIterableIterator { + while (true) { + const action = Pseudorandom.sample(FUZZ_ACTIONS)!; + yield (await action!.next()).value; + } + } + }(simulationEnvironment); + simulation.resets = true; + return simulation.fuzzAsync(); + }); +}); +// tslint:disable-next-line max-file-line-count diff --git a/contracts/integrations/test/wrappers.ts b/contracts/integrations/test/wrappers.ts index 5caf575641..f466ee2b90 100644 --- a/contracts/integrations/test/wrappers.ts +++ b/contracts/integrations/test/wrappers.ts @@ -8,6 +8,7 @@ export * from '../test/generated-wrappers/test_eth2_dai'; export * from '../test/generated-wrappers/test_eth2_dai_bridge'; export * from '../test/generated-wrappers/test_framework'; export * from '../test/generated-wrappers/test_mainnet_aggregator_fills'; +export * from '../test/generated-wrappers/test_signature_validation_wallet'; export * from '../test/generated-wrappers/test_uniswap_bridge'; export * from '../test/generated-wrappers/test_uniswap_exchange'; export * from '../test/generated-wrappers/test_uniswap_exchange_factory'; diff --git a/contracts/integrations/tsconfig.json b/contracts/integrations/tsconfig.json index 6b1f4e552c..8f879b58aa 100644 --- a/contracts/integrations/tsconfig.json +++ b/contracts/integrations/tsconfig.json @@ -9,6 +9,7 @@ "test/generated-artifacts/TestEth2DaiBridge.json", "test/generated-artifacts/TestFramework.json", "test/generated-artifacts/TestMainnetAggregatorFills.json", + "test/generated-artifacts/TestSignatureValidationWallet.json", "test/generated-artifacts/TestUniswapBridge.json", "test/generated-artifacts/TestUniswapExchange.json", "test/generated-artifacts/TestUniswapExchangeFactory.json"