import { ExchangeContract } from '@0x/contracts-exchange'; import { blockchainTests, constants, expect, orderHashUtils, signingUtils, transactionHashUtils, } from '@0x/contracts-test-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.getOrderHashHex(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.getOrderHashHex(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