protocol/contracts/integrations/test/fuzz_tests/exchange_signature_validation_test.ts

732 lines
28 KiB
TypeScript

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<void> {
await exchange.preSign(hash).awaitTransactionSuccessAsync({ from: signer });
}
async function approveValidatorAsync(signer: string, validator: string, approved: boolean = true): Promise<void> {
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<SignatureTestParams> {
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> = {}): 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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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> = {}): 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<SignatureTestParams> = {},
): Promise<SignatureTestParams & { order: Order }> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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> = {}): 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<SignatureTestParams> = {},
): Promise<SignatureTestParams & { transaction: ZeroExTransaction }> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<AssertionResult | void> {
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