From 0c08be911afd28ea2826646f893495f2b22d79b2 Mon Sep 17 00:00:00 2001 From: Federico Giacon <58218759+fedgiac@users.noreply.github.com> Date: Thu, 8 Aug 2024 09:41:44 +0000 Subject: [PATCH] chore: migrate GPv2Trade extractOrder tests to Foundry --- test/GPv2Trade.test.ts | 99 +------------------------ test/GPv2Trade/ExtractOrder.t.sol | 115 ++++++++++++++++++++++++++++++ test/GPv2Trade/Helper.sol | 23 ++++++ 3 files changed, 140 insertions(+), 97 deletions(-) create mode 100644 test/GPv2Trade/ExtractOrder.t.sol create mode 100644 test/GPv2Trade/Helper.sol diff --git a/test/GPv2Trade.test.ts b/test/GPv2Trade.test.ts index 7fbe7a95..3172f4b4 100644 --- a/test/GPv2Trade.test.ts +++ b/test/GPv2Trade.test.ts @@ -1,11 +1,10 @@ import { expect } from "chai"; -import { Contract, BigNumber } from "ethers"; -import { ethers, waffle } from "hardhat"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; import { OrderBalance, OrderKind, - SettlementEncoder, SigningScheme, encodeOrderFlags, encodeSigningScheme, @@ -15,34 +14,9 @@ import { OrderBalanceId, decodeOrderKind, decodeOrderBalance, - decodeOrder, } from "./encoding"; -function fillBytes(count: number, byte: number): string { - return ethers.utils.hexlify([...Array(count)].map(() => byte)); -} - -function fillUint(bits: number, byte: number): BigNumber { - return BigNumber.from(fillBytes(bits / 8, byte)); -} - describe("GPv2Trade", () => { - const [, ...traders] = waffle.provider.getWallets(); - - const testDomain = { name: "test" }; - const sampleOrder = { - sellToken: fillBytes(20, 0x01), - buyToken: fillBytes(20, 0x02), - receiver: fillBytes(20, 0x03), - sellAmount: ethers.utils.parseEther("42"), - buyAmount: ethers.utils.parseEther("13.37"), - validTo: 0xffffffff, - appData: ethers.constants.HashZero, - feeAmount: ethers.utils.parseEther("1.0"), - kind: OrderKind.SELL, - partiallyFillable: false, - }; - let tradeLib: Contract; beforeEach(async () => { @@ -51,75 +25,6 @@ describe("GPv2Trade", () => { tradeLib = await GPv2Trade.deploy(); }); - describe("extractOrder", () => { - 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 encodedOrder = await tradeLib.extractOrderTest( - encoder.tokens, - encoder.trades[0], - ); - expect(decodeOrder(encodedOrder)).to.deep.equal(order); - }); - - it("should revert for invalid sell token indices", async () => { - const encoder = new SettlementEncoder(testDomain); - await encoder.signEncodeTrade( - sampleOrder, - traders[1], - SigningScheme.EIP712, - ); - - const tokens = encoder.tokens.filter( - (token) => token !== sampleOrder.sellToken, - ); - await expect(tradeLib.extractOrderTest(tokens, encoder.trades)).to.be - .reverted; - }); - - it("should revert for invalid buy token indices", async () => { - const encoder = new SettlementEncoder(testDomain); - await encoder.signEncodeTrade( - sampleOrder, - traders[1], - SigningScheme.EIP712, - ); - - const tokens = encoder.tokens.filter( - (token) => token !== sampleOrder.buyToken, - ); - await expect(tradeLib.extractOrderTest(tokens, encoder.trades)).to.be - .reverted; - }); - }); - describe("extractFlags", () => { it("should extract all supported order flags", async () => { const flagVariants = [OrderKind.SELL, OrderKind.BUY].flatMap((kind) => diff --git a/test/GPv2Trade/ExtractOrder.t.sol b/test/GPv2Trade/ExtractOrder.t.sol new file mode 100644 index 00000000..8e9cd812 --- /dev/null +++ b/test/GPv2Trade/ExtractOrder.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {GPv2Order, IERC20} from "src/contracts/libraries/GPv2Trade.sol"; +import {GPv2Signing} from "src/contracts/mixins/GPv2Signing.sol"; + +import {Helper} from "./Helper.sol"; + +import {Order as OrderLib} from "test/libraries/Order.sol"; +import {SettlementEncoder} from "test/libraries/encoders/SettlementEncoder.sol"; + +contract ExtractOrder is Helper { + using SettlementEncoder for SettlementEncoder.State; + + struct Fuzzed { + address sellToken; + address buyToken; + address receiver; + uint256 sellAmount; + uint256 buyAmount; + uint32 validTo; + bytes32 appData; + uint256 feeAmount; + uint256 executedAmount; + } + + function sampleOrder() private returns (GPv2Order.Data memory order) { + order = GPv2Order.Data({ + sellToken: IERC20(makeAddr("GPv2Trade.ExtractOrder sampleOrder sell token")), + buyToken: IERC20(makeAddr("GPv2Trade.ExtractOrder sampleOrder buy token")), + receiver: makeAddr("GPv2Trade.ExtractOrder sampleOrder receiver"), + sellAmount: 42 ether, + buyAmount: 13.37 ether, + validTo: type(uint32).max, + appData: bytes32(0), + feeAmount: 1 ether, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }); + } + + function assertSameOrder(GPv2Order.Data memory lhs, GPv2Order.Data memory rhs) private pure { + uint256 assertionCount = 12; + assertEq(address(lhs.sellToken), address(rhs.sellToken)); + assertEq(address(lhs.buyToken), address(rhs.buyToken)); + assertEq(lhs.receiver, rhs.receiver); + assertEq(lhs.sellAmount, rhs.sellAmount); + assertEq(lhs.buyAmount, rhs.buyAmount); + assertEq(lhs.validTo, rhs.validTo); + assertEq(lhs.appData, rhs.appData); + assertEq(lhs.feeAmount, rhs.feeAmount); + assertEq(lhs.kind, rhs.kind); + assertEq(lhs.partiallyFillable, rhs.partiallyFillable); + assertEq(lhs.sellTokenBalance, rhs.sellTokenBalance); + assertEq(lhs.buyTokenBalance, rhs.buyTokenBalance); + // We want to make sure that if we add new fields to `GPv2Order.Data` we + // don't forget to add the corresponding assertion. + // This only works because all fields are _not_ of dynamic length. + require(abi.encode(lhs).length == assertionCount * 32, "bad test setup when comparing two orders"); + } + + function testFuzz_should_round_trip_encode_order_data(Fuzzed memory fuzzed) public { + OrderLib.Flags[] memory flags = OrderLib.ALL_FLAGS(); + + for (uint256 i = 0; i < flags.length; i++) { + GPv2Order.Data memory order = GPv2Order.Data({ + sellToken: IERC20(fuzzed.sellToken), + buyToken: IERC20(fuzzed.buyToken), + receiver: fuzzed.receiver, + sellAmount: fuzzed.sellAmount, + buyAmount: fuzzed.buyAmount, + validTo: fuzzed.validTo, + appData: fuzzed.appData, + feeAmount: fuzzed.feeAmount, + kind: flags[i].kind, + partiallyFillable: flags[i].partiallyFillable, + sellTokenBalance: flags[i].sellTokenBalance, + buyTokenBalance: flags[i].buyTokenBalance + }); + + SettlementEncoder.State storage encoder = SettlementEncoder.makeSettlementEncoder(); + encoder.signEncodeTrade( + vm, trader, order, domainSeparator, GPv2Signing.Scheme.Eip712, fuzzed.executedAmount + ); + GPv2Order.Data memory extractedOrder = executor.extractOrderTest(encoder.tokens(), encoder.trades[0]); + assertSameOrder(order, extractedOrder); + } + } + + function should_revert_for_invalid_token_indices(GPv2Order.Data memory order, IERC20[] memory tokens) internal { + SettlementEncoder.State storage encoder = SettlementEncoder.makeSettlementEncoder(); + encoder.signEncodeTrade(vm, trader, order, domainSeparator, GPv2Signing.Scheme.Eip712, 0); + // TODO: once Foundry supports catching EVM errors, require that this + // reverts with "array out-of-bounds access". + // Track support at https://github.com/foundry-rs/foundry/issues/4012 + vm.expectRevert(); + executor.extractOrderTest(tokens, encoder.trades[0]); + } + + function test_should_revert_for_invalid_sell_token_indices() public { + GPv2Order.Data memory order = sampleOrder(); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = order.buyToken; + should_revert_for_invalid_token_indices(order, tokens); + } + + function test_should_revert_for_invalid_buy_token_indices() public { + GPv2Order.Data memory order = sampleOrder(); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = order.sellToken; + should_revert_for_invalid_token_indices(order, tokens); + } +} diff --git a/test/GPv2Trade/Helper.sol b/test/GPv2Trade/Helper.sol new file mode 100644 index 00000000..e494aaf8 --- /dev/null +++ b/test/GPv2Trade/Helper.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8; + +import {Test, Vm} from "forge-std/Test.sol"; + +import {SettlementEncoder} from "test/libraries/encoders/SettlementEncoder.sol"; +import {GPv2TradeTestInterface} from "test/src/GPv2TradeTestInterface.sol"; + +// TODO: move the content of `GPv2TradeTestInterface` here once all tests have been removed. +// solhint-disable-next-line no-empty-blocks +contract Harness is GPv2TradeTestInterface {} + +contract Helper is Test { + Harness internal executor; + Vm.Wallet internal trader = vm.createWallet("GPv2Trade.Helper trader"); + bytes32 internal domainSeparator = keccak256("GPv2Trade.Helper domain separator"); + SettlementEncoder.State internal encoder; + + function setUp() public { + executor = new Harness(); + encoder = SettlementEncoder.makeSettlementEncoder(); + } +}