-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: migrate GPv2Order eip712 tests to Foundry (#195)
## Description See title. This part was particularly painful to port to Solidity; most notably, there are a lot of missing quality-of-life features when handling arrays. Since we don't have `ethers` anymore, I had to build my own EIP712 encoding library (as [recommended by Foundry](https://book.getfoundry.sh/tutorials/testing-eip712)). As I could have introduced new bugs in the library, the related tests have become more aggressive (for example, all flag combinations are tested for consistency with the contract). There are also a few sanity checks which have already turned out to be useful during development. I also sneakily introduced fuzz tests, since they seemed appropriate here. Sorry for the horrible nested loops, suggestions on how to get rid of them are welcome. The good news: there's a chance that a few things can be simplified with a future version of the compiler, see comments in code. ## Test Plan CI. ## Related Issues #117 --------- Co-authored-by: mfw78 <[email protected]>
- Loading branch information
Showing
8 changed files
with
272 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// SPDX-License-Identifier: LGPL-3.0-or-later | ||
pragma solidity ^0.8; | ||
|
||
import {Test} from "forge-std/Test.sol"; | ||
|
||
import {GPv2OrderTestInterface} from "test/src/GPv2OrderTestInterface.sol"; | ||
|
||
// TODO: move the content of `GPv2OrderTestInterface` here once all tests have been removed. | ||
// solhint-disable-next-line no-empty-blocks | ||
contract Harness is GPv2OrderTestInterface {} | ||
|
||
contract Helper is Test { | ||
Harness internal executor; | ||
|
||
function setUp() public { | ||
executor = new Harness(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// SPDX-License-Identifier: LGPL-3.0-or-later | ||
pragma solidity ^0.8; | ||
|
||
import {Helper} from "./Helper.sol"; | ||
|
||
import {GPv2Order, IERC20} from "src/contracts/libraries/GPv2Order.sol"; | ||
|
||
import {Helper} from "./Helper.sol"; | ||
import {Eip712} from "test/libraries/Eip712.sol"; | ||
import {Order as OrderLib} from "test/libraries/Order.sol"; | ||
|
||
contract Order is Helper { | ||
using Eip712 for GPv2Order.Data; | ||
using Eip712 for Eip712.Order; | ||
|
||
struct Fuzzed { | ||
address sellToken; | ||
address buyToken; | ||
address receiver; | ||
uint256 sellAmount; | ||
uint256 buyAmount; | ||
uint32 validTo; | ||
bytes32 appData; | ||
uint256 feeAmount; | ||
} | ||
|
||
// Keep track of which order hashes have been seen. | ||
mapping(bytes32 orderHash => bool) seen; | ||
|
||
function test_TYPE_HASH_matches_the_EIP_712_order_type_hash() public view { | ||
assertEq(executor.typeHashTest(), Eip712.ORDER_TYPE_HASH()); | ||
} | ||
|
||
function testFuzz_computes_EIP_712_order_signing_hash(Fuzzed memory fuzzed) public { | ||
bytes32 domainSeparator = keccak256("test domain separator"); | ||
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 | ||
}); | ||
|
||
bytes32 orderSigningHash = executor.hashTest(order, domainSeparator); | ||
assertEq(orderSigningHash, order.toEip712SignedStruct().typedDataHash(domainSeparator)); | ||
require(!seen[orderSigningHash], "different flags led to the same hash"); | ||
seen[orderSigningHash] = true; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// SPDX-License-Identifier: LGPL-3.0-or-later | ||
pragma solidity ^0.8; | ||
|
||
import {GPv2Order} from "src/contracts/libraries/GPv2Order.sol"; | ||
|
||
library Eip712 { | ||
// This is the struct representing an order that is signed by the user using | ||
// EIP-712. | ||
struct Order { | ||
address sellToken; | ||
address buyToken; | ||
address receiver; | ||
uint256 sellAmount; | ||
uint256 buyAmount; | ||
uint32 validTo; | ||
bytes32 appData; | ||
uint256 feeAmount; | ||
string kind; | ||
bool partiallyFillable; | ||
string sellTokenBalance; | ||
string buyTokenBalance; | ||
} | ||
|
||
// Ideally, this would be replaced by type(Order).typehash. | ||
// Progress tracking for this Solidity feature is here: | ||
// https://github.com/ethereum/solidity/issues/14157 | ||
function ORDER_TYPE_HASH() internal pure returns (bytes32) { | ||
return keccak256( | ||
bytes( | ||
string.concat( | ||
// Should reflect the definition of the struct `Order`. | ||
"Order(", | ||
"address sellToken,", | ||
"address buyToken,", | ||
"address receiver,", | ||
"uint256 sellAmount,", | ||
"uint256 buyAmount,", | ||
"uint32 validTo,", | ||
"bytes32 appData,", | ||
"uint256 feeAmount,", | ||
"string kind,", | ||
"bool partiallyFillable,", | ||
"string sellTokenBalance,", | ||
"string buyTokenBalance", | ||
")" | ||
) | ||
) | ||
); | ||
} | ||
|
||
function toKindString(bytes32 orderKind) internal pure returns (string memory) { | ||
if (orderKind == GPv2Order.KIND_SELL) { | ||
return "sell"; | ||
} else if (orderKind == GPv2Order.KIND_BUY) { | ||
return "buy"; | ||
} else { | ||
revert("invalid order kind identifier"); | ||
} | ||
} | ||
|
||
function toSellTokenBalanceString(bytes32 balanceType) private pure returns (string memory) { | ||
return toTokenBalanceString(balanceType, true); | ||
} | ||
|
||
function toBuyTokenBalanceString(bytes32 balanceType) private pure returns (string memory) { | ||
return toTokenBalanceString(balanceType, false); | ||
} | ||
|
||
function toTokenBalanceString(bytes32 balanceType, bool isSell) internal pure returns (string memory) { | ||
if (balanceType == GPv2Order.BALANCE_ERC20) { | ||
return "erc20"; | ||
} else if (balanceType == GPv2Order.BALANCE_EXTERNAL) { | ||
require(isSell, "external order kind is only supported for sell balance"); | ||
return "external"; | ||
} else if (balanceType == GPv2Order.BALANCE_INTERNAL) { | ||
return "internal"; | ||
} else { | ||
revert("invalid order kind identifier"); | ||
} | ||
} | ||
|
||
function toEip712SignedStruct(GPv2Order.Data memory order) internal pure returns (Order memory) { | ||
return Order({ | ||
sellToken: address(order.sellToken), | ||
buyToken: address(order.buyToken), | ||
receiver: order.receiver, | ||
sellAmount: order.sellAmount, | ||
buyAmount: order.buyAmount, | ||
validTo: order.validTo, | ||
appData: order.appData, | ||
feeAmount: order.feeAmount, | ||
kind: toKindString(order.kind), | ||
partiallyFillable: order.partiallyFillable, | ||
sellTokenBalance: toSellTokenBalanceString(order.sellTokenBalance), | ||
buyTokenBalance: toBuyTokenBalanceString(order.buyTokenBalance) | ||
}); | ||
} | ||
|
||
function hashStruct(Order memory order) internal pure returns (bytes32) { | ||
// Ideally, this would be replaced by `order.hashStruct()`. | ||
// Progress tracking for this Solidity feature is here: | ||
// https://github.com/ethereum/solidity/issues/14208 | ||
return keccak256( | ||
// Note: dynamic types are hashed. | ||
abi.encode( | ||
ORDER_TYPE_HASH(), | ||
order.sellToken, | ||
order.buyToken, | ||
order.receiver, | ||
order.sellAmount, | ||
order.buyAmount, | ||
order.validTo, | ||
order.appData, | ||
order.feeAmount, | ||
keccak256(bytes(order.kind)), | ||
order.partiallyFillable, | ||
keccak256(bytes(order.sellTokenBalance)), | ||
keccak256(bytes(order.buyTokenBalance)) | ||
) | ||
); | ||
} | ||
|
||
// Ideally, this would be replaced by a dedicated function in Solidity. | ||
// This is currently not planned but it could be once `typehash` and | ||
// `hashStruct` are introduced. | ||
function typedDataHash(Order memory order, bytes32 domainSeparator) internal pure returns (bytes32) { | ||
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, hashStruct(order))); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters