Skip to content

Commit

Permalink
chore: migrate GPv2Signing recoverOrderFromTrade tests to Foundry (#204)
Browse files Browse the repository at this point in the history
## Description

See title.
This also introduces a small library to make it easier to create a test
that uses fuzzed orders.
Fuzzed tests are used here because the exact bytes used in the tests are
important, and fuzzing allows us to check if changing some bytes affects
the test output. Previously, the sample order in the test was built by
filling each byte with a nonzero value.

I also merged the tests "should round-trip encode order data" and
"should compute order unique identifier" because it was just one line
more in the first test, and now that we do fuzzing it's more efficient
to do both in the same test rather than running independent fuzzes on
both.

Here the signing and encoding libraries proved to be very nice to use!

## Test Plan

CI.

## Related Issues

#120

---------

Co-authored-by: mfw78 <[email protected]>
  • Loading branch information
fedgiac and mfw78 authored Aug 15, 2024
1 parent 0e3dd9b commit 1d1628c
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 109 deletions.
111 changes: 2 additions & 109 deletions test/GPv2Signing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { artifacts, ethers, waffle } from "hardhat";

import {
EIP1271_MAGICVALUE,
OrderBalance,
OrderKind,
SettlementEncoder,
SigningScheme,
TypedDataDomain,
Expand All @@ -16,8 +14,8 @@ import {
signOrder,
} from "../src/ts";

import { decodeOrder, encodeOrder } from "./encoding";
import { fillBytes, fillUint, SAMPLE_ORDER } from "./testHelpers";
import { encodeOrder } from "./encoding";
import { SAMPLE_ORDER } from "./testHelpers";

describe("GPv2Signing", () => {
const [deployer, ...traders] = waffle.provider.getWallets();
Expand All @@ -37,111 +35,6 @@ describe("GPv2Signing", () => {
});

describe("recoverOrderFromTrade", () => {
it("should round-trip encode order data", async () => {
// NOTE: Pay extra attention to use all bytes for each field, and that
// they all have different values to make sure the are correctly
// round-tripped.
const order = {
sellToken: fillBytes(20, 0x01),
buyToken: fillBytes(20, 0x02),
receiver: fillBytes(20, 0x03),
sellAmount: fillUint(256, 0x04),
buyAmount: fillUint(256, 0x05),
validTo: fillUint(32, 0x06).toNumber(),
appData: fillBytes(32, 0x07),
feeAmount: fillUint(256, 0x08),
kind: OrderKind.BUY,
partiallyFillable: true,
sellTokenBalance: OrderBalance.EXTERNAL,
buyTokenBalance: OrderBalance.INTERNAL,
};
const tradeExecution = {
executedAmount: fillUint(256, 0x09),
};

const encoder = new SettlementEncoder(testDomain);
await encoder.signEncodeTrade(
order,
traders[0],
SigningScheme.EIP712,
tradeExecution,
);

const { data: encodedOrder } = await signing.recoverOrderFromTradeTest(
encoder.tokens,
encoder.trades[0],
);
expect(decodeOrder(encodedOrder)).to.deep.equal(order);
});

it("should compute order unique identifier", async () => {
const encoder = new SettlementEncoder(testDomain);
await encoder.signEncodeTrade(
SAMPLE_ORDER,
traders[0],
SigningScheme.EIP712,
);

const { uid: orderUid } = await signing.recoverOrderFromTradeTest(
encoder.tokens,
encoder.trades[0],
);
expect(orderUid).to.equal(
computeOrderUid(testDomain, SAMPLE_ORDER, traders[0].address),
);
});

it("should recover the owner for all signing schemes", async () => {
const artifact = await artifacts.readArtifact("EIP1271Verifier");
const verifier = await waffle.deployMockContract(deployer, artifact.abi);
await verifier.mock.isValidSignature.returns(EIP1271_MAGICVALUE);

const sampleOrderUid = computeOrderUid(
testDomain,
SAMPLE_ORDER,
traders[2].address,
);
await signing.connect(traders[2]).setPreSignature(sampleOrderUid, true);

const encoder = new SettlementEncoder(testDomain);
await encoder.signEncodeTrade(
SAMPLE_ORDER,
traders[0],
SigningScheme.EIP712,
);
await encoder.signEncodeTrade(
SAMPLE_ORDER,
traders[1],
SigningScheme.ETHSIGN,
);
encoder.encodeTrade(SAMPLE_ORDER, {
scheme: SigningScheme.EIP1271,
data: {
verifier: verifier.address,
signature: "0x",
},
});
encoder.encodeTrade(SAMPLE_ORDER, {
scheme: SigningScheme.PRESIGN,
data: traders[2].address,
});

const owners = [
traders[0].address,
traders[1].address,
verifier.address,
traders[2].address,
];

for (const [i, trade] of encoder.trades.entries()) {
const { owner } = await signing.recoverOrderFromTradeTest(
encoder.tokens,
trade,
);
expect(owner).to.equal(owners[i]);
}
});

describe("uid uniqueness", () => {
it("invalid EVM transaction encoding does not change order hash", async () => {
// The variables for an EVM transaction are encoded in multiples of 32
Expand Down
2 changes: 2 additions & 0 deletions test/GPv2Signing/Helper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ contract Harness is GPv2SigningTestInterface {}

contract Helper is Test {
Harness internal executor;
bytes32 internal domainSeparator;

function setUp() public {
executor = new Harness();
domainSeparator = executor.domainSeparator();
}
}
73 changes: 73 additions & 0 deletions test/GPv2Signing/RecoverOrderFromTrade.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
pragma solidity ^0.8;

import {Vm} from "forge-std/Test.sol";

import {EIP1271Verifier, GPv2EIP1271, GPv2Order, GPv2Signing} from "src/contracts/mixins/GPv2Signing.sol";

import {Helper} from "./Helper.sol";
import {Order} from "test/libraries/Order.sol";
import {Sign} from "test/libraries/Sign.sol";
import {SettlementEncoder} from "test/libraries/encoders/SettlementEncoder.sol";

contract RecoverOrderFromTrade is Helper {
using SettlementEncoder for SettlementEncoder.State;
using Sign for EIP1271Verifier;

Vm.Wallet private trader;

constructor() {
trader = vm.createWallet("GPv2Signing.RecoverOrderFromTrade: trader");
}

function test_should_round_trip_encode_order_data_and_unique_identifier(
Order.Fuzzed memory params,
uint256 executedAmount
) public {
GPv2Order.Data memory order = Order.fuzz(params);

SettlementEncoder.State storage encoder = SettlementEncoder.makeSettlementEncoder();
encoder.signEncodeTrade(vm, trader, order, domainSeparator, GPv2Signing.Scheme.Eip712, executedAmount);

GPv2Signing.RecoveredOrder memory recovered =
executor.recoverOrderFromTradeTest(encoder.tokens(), encoder.trades[0]);
assertEq(abi.encode(recovered.data), abi.encode(order));
assertEq(recovered.uid, Order.computeOrderUid(order, domainSeparator, trader.addr));
}

function test_should_recover_the_order_for_all_signing_schemes(Order.Fuzzed memory params) public {
GPv2Order.Data memory order = Order.fuzz(params);

address traderPreSign = makeAddr("trader pre-sign");
EIP1271Verifier traderEip1271 = EIP1271Verifier(makeAddr("eip1271 verifier"));
Vm.Wallet memory traderEip712 = vm.createWallet("trader eip712");
Vm.Wallet memory traderEthsign = vm.createWallet("trader ethsign");

bytes memory uidPreSign = Order.computeOrderUid(order, domainSeparator, traderPreSign);
vm.prank(traderPreSign);
executor.setPreSignature(uidPreSign, true);

vm.mockCallRevert(address(traderEip1271), hex"", "unexpected call to mock contract");
vm.mockCall(
address(traderEip1271),
abi.encodePacked(EIP1271Verifier.isValidSignature.selector),
abi.encode(GPv2EIP1271.MAGICVALUE)
);

SettlementEncoder.State storage encoder = SettlementEncoder.makeSettlementEncoder();
encoder.encodeTrade(order, Sign.preSign(traderPreSign), 0);
encoder.encodeTrade(order, Sign.sign(traderEip1271, hex""), 0);
encoder.signEncodeTrade(vm, traderEip712, order, domainSeparator, GPv2Signing.Scheme.Eip712, 0);
encoder.signEncodeTrade(vm, traderEthsign, order, domainSeparator, GPv2Signing.Scheme.EthSign, 0);

GPv2Signing.RecoveredOrder memory recovered;
recovered = executor.recoverOrderFromTradeTest(encoder.tokens(), encoder.trades[0]);
assertEq(recovered.owner, traderPreSign);
recovered = executor.recoverOrderFromTradeTest(encoder.tokens(), encoder.trades[1]);
assertEq(recovered.owner, address(traderEip1271));
recovered = executor.recoverOrderFromTradeTest(encoder.tokens(), encoder.trades[2]);
assertEq(recovered.owner, traderEip712.addr);
recovered = executor.recoverOrderFromTradeTest(encoder.tokens(), encoder.trades[3]);
assertEq(recovered.owner, traderEthsign.addr);
}
}
34 changes: 34 additions & 0 deletions test/libraries/Order.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ library Order {
bool partiallyFillable;
}

/// All parameters needed to generated a valid fuzzed order.
struct Fuzzed {
address sellToken;
address buyToken;
address receiver;
uint256 sellAmount;
uint256 buyAmount;
uint32 validTo;
bytes32 appData;
uint256 feeAmount;
bytes32 flagsPick;
}

// I wish I could declare the following as constants and export them as part
// of the library. However, "Only constants of value type and byte array
// type are implemented." and "Library cannot have non-constant state
Expand Down Expand Up @@ -145,4 +158,25 @@ library Order {
orderUid = new bytes(GPv2Order.UID_LENGTH);
orderUid.packOrderUidParams(orderHash, owner, validTo);
}

function fuzz(Fuzzed memory params) internal pure returns (GPv2Order.Data memory) {
Order.Flags[] memory allFlags = Order.ALL_FLAGS();
// `flags` isn't exactly random, but for fuzzing purposes it should be
// more than enough.
Order.Flags memory flags = allFlags[uint256(params.flagsPick) % allFlags.length];
return GPv2Order.Data({
sellToken: IERC20(params.sellToken),
buyToken: IERC20(params.buyToken),
receiver: params.receiver,
sellAmount: params.sellAmount,
buyAmount: params.buyAmount,
validTo: params.validTo,
appData: params.appData,
feeAmount: params.feeAmount,
partiallyFillable: flags.partiallyFillable,
kind: flags.kind,
sellTokenBalance: flags.sellTokenBalance,
buyTokenBalance: flags.buyTokenBalance
});
}
}

0 comments on commit 1d1628c

Please sign in to comment.