From ff795e8b3fc74a8e2bbec03616ce23230592bfb9 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Wed, 17 Jul 2024 20:27:46 +0100 Subject: [PATCH 01/18] feat: permit2 for token flows --- .gas-snapshot | 51 +++++---- .github/workflows/cd.yml | 4 +- script/Zenith.s.sol | 17 +-- src/IOrders.sol | 28 +++++ src/Orders.sol | 98 ++++++++++------- src/Passage.sol | 59 ++++++++-- src/UsesPermit2.sol | 162 ++++++++++++++++++++++++++++ src/vendored/IEIP712.sol | 6 ++ src/vendored/ISignatureTransfer.sol | 134 +++++++++++++++++++++++ test/Orders.t.sol | 37 +++---- test/Passage.t.sol | 4 +- test/Transact.t.sol | 2 +- 12 files changed, 496 insertions(+), 106 deletions(-) create mode 100644 src/IOrders.sol create mode 100644 src/UsesPermit2.sol create mode 100644 src/vendored/IEIP712.sol create mode 100644 src/vendored/ISignatureTransfer.sol diff --git a/.gas-snapshot b/.gas-snapshot index cfa160e..0bc8776 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,32 +1,31 @@ -OrdersTest:test_fill_ERC20() (gas: 70364) -OrdersTest:test_fill_ETH() (gas: 68414) -OrdersTest:test_fill_both() (gas: 166580) -OrdersTest:test_fill_multiETH() (gas: 131926) -OrdersTest:test_fill_underflowETH() (gas: 115281) -OrdersTest:test_initiate_ERC20() (gas: 81435) -OrdersTest:test_initiate_ETH() (gas: 44949) -OrdersTest:test_initiate_both() (gas: 118677) -OrdersTest:test_initiate_multiERC20() (gas: 722408) -OrdersTest:test_initiate_multiETH() (gas: 75304) -OrdersTest:test_onlyBuilder() (gas: 12815) -OrdersTest:test_orderExpired() (gas: 27956) -OrdersTest:test_sweepERC20() (gas: 60446) -OrdersTest:test_sweepETH() (gas: 81940) -OrdersTest:test_underflowETH() (gas: 63528) -PassageTest:test_configureEnter() (gas: 82311) -PassageTest:test_disallowedEnter() (gas: 17938) +OrdersTest:test_fill_ERC20() (gas: 70515) +OrdersTest:test_fill_ETH() (gas: 68476) +OrdersTest:test_fill_both() (gas: 166751) +OrdersTest:test_fill_multiETH() (gas: 132097) +OrdersTest:test_fill_underflowETH() (gas: 115381) +OrdersTest:test_initiate_ERC20() (gas: 81614) +OrdersTest:test_initiate_ETH() (gas: 45128) +OrdersTest:test_initiate_both() (gas: 118889) +OrdersTest:test_initiate_multiERC20() (gas: 722620) +OrdersTest:test_initiate_multiETH() (gas: 75516) +OrdersTest:test_orderExpired() (gas: 28084) +OrdersTest:test_sweepERC20() (gas: 60469) +OrdersTest:test_sweepETH() (gas: 82142) +OrdersTest:test_underflowETH() (gas: 63668) +PassageTest:test_configureEnter() (gas: 125672) +PassageTest:test_disallowedEnter() (gas: 56597) PassageTest:test_enter() (gas: 25507) -PassageTest:test_enterToken() (gas: 64354) -PassageTest:test_enterToken_defaultChain() (gas: 62870) -PassageTest:test_enter_defaultChain() (gas: 24011) -PassageTest:test_fallback() (gas: 21445) +PassageTest:test_enterToken() (gas: 64397) +PassageTest:test_enterToken_defaultChain() (gas: 62935) +PassageTest:test_enter_defaultChain() (gas: 24033) +PassageTest:test_fallback() (gas: 21489) PassageTest:test_onlyTokenAdmin() (gas: 16881) -PassageTest:test_receive() (gas: 21339) -PassageTest:test_setUp() (gas: 16901) +PassageTest:test_receive() (gas: 21361) +PassageTest:test_setUp() (gas: 16923) PassageTest:test_withdraw() (gas: 59188) -RollupPassageTest:test_exit() (gas: 22347) -RollupPassageTest:test_exitToken() (gas: 50183) -RollupPassageTest:test_fallback() (gas: 19883) +RollupPassageTest:test_exit() (gas: 22369) +RollupPassageTest:test_exitToken() (gas: 50214) +RollupPassageTest:test_fallback() (gas: 19905) RollupPassageTest:test_receive() (gas: 19844) TransactTest:test_configureGas() (gas: 22828) TransactTest:test_enterTransact() (gas: 103961) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 15937ec..ca36015 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -50,8 +50,8 @@ jobs: environment: dev forge-deployment-contract: ZenithScript forge-deployment-script-file: Zenith.s.sol - forge-deployment-signature: "deploy(uint256,address,address[],address)" - forge-deployment-params: "17001 0x11Aa4EBFbf7a481617c719a2Df028c9DA1a219aa [] 0x29403F107781ea45Bf93710abf8df13F67f2008f" + forge-deployment-signature: "deploy(uint256,address,address[],address,address)" + forge-deployment-params: "17001 0x11Aa4EBFbf7a481617c719a2Df028c9DA1a219aa [] 0x29403F107781ea45Bf93710abf8df13F67f2008f 0x000000000022D473030F116dDEE9F6B43aC78BA3" etherscan-url: https://holesky.etherscan.io chain-id: 17000 deployer-address: ${{ vars.HOLESKY_DEPLOYER_ADDRESS }} diff --git a/script/Zenith.s.sol b/script/Zenith.s.sol index 8bc7622..6c4f3bf 100644 --- a/script/Zenith.s.sol +++ b/script/Zenith.s.sol @@ -9,26 +9,27 @@ import {HostOrders, RollupOrders} from "../src/Orders.sol"; contract ZenithScript is Script { // deploy: - // forge script ZenithScript --sig "deploy(uint256,address,address)" --rpc-url $RPC_URL --etherscan-api-key $ETHERSCAN_API_KEY --private-key $PRIVATE_KEY --broadcast --verify $ROLLUP_CHAIN_ID $WITHDRAWAL_ADMIN_ADDRESS $INITIAL_ENTER_TOKENS_ARRAY $SEQUENCER_AND_GAS_ADMIN_ADDRESS + // forge script ZenithScript --sig "deploy(uint256,address,address[],address,address)" --rpc-url $RPC_URL --etherscan-api-key $ETHERSCAN_API_KEY --private-key $PRIVATE_KEY --broadcast --verify $ROLLUP_CHAIN_ID $WITHDRAWAL_ADMIN_ADDRESS $INITIAL_ENTER_TOKENS_ARRAY $SEQUENCER_AND_GAS_ADMIN_ADDRESS $PERMIT_2 function deploy( uint256 defaultRollupChainId, address withdrawalAdmin, address[] memory initialEnterTokens, - address sequencerAndGasAdmin + address sequencerAndGasAdmin, + address permit2 ) public returns (Zenith z, Passage p, Transactor t, HostOrders m) { vm.startBroadcast(); z = new Zenith(sequencerAndGasAdmin); - p = new Passage(defaultRollupChainId, withdrawalAdmin, initialEnterTokens); + p = new Passage(defaultRollupChainId, withdrawalAdmin, initialEnterTokens, permit2); t = new Transactor(defaultRollupChainId, sequencerAndGasAdmin, p, 30_000_000, 5_000_000); - m = new HostOrders(); + m = new HostOrders(permit2); } // deploy: - // forge script ZenithScript --sig "deployL2()" --rpc-url $L2_RPC_URL --private-key $PRIVATE_KEY --broadcast - function deployL2() public returns (RollupPassage p, RollupOrders m) { + // forge script ZenithScript --sig "deployL2(address)" --rpc-url $L2_RPC_URL --private-key $PRIVATE_KEY --broadcast $PERMIT_2 + function deployL2(address permit2) public returns (RollupPassage p, RollupOrders m) { vm.startBroadcast(); - p = new RollupPassage(); - m = new RollupOrders(); + p = new RollupPassage(permit2); + m = new RollupOrders(permit2); } // NOTE: script must be run using SequencerAdmin key diff --git a/src/IOrders.sol b/src/IOrders.sol new file mode 100644 index 0000000..d4d8030 --- /dev/null +++ b/src/IOrders.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +interface IOrders { + /// @notice Tokens sent by the swapper as inputs to the order + /// @dev From ERC-7683 + struct Input { + /// @dev The address of the ERC20 token on the origin chain + address token; + /// @dev The amount of the token to be sent + uint256 amount; + } + + /// @notice Tokens that must be receive for a valid order fulfillment + /// @dev From ERC-7683 + struct Output { + /// @dev The address of the ERC20 token on the destination chain + /// @dev address(0) used as a sentinel for the native token + address token; + /// @dev The amount of the token to be sent + uint256 amount; + /// @dev The address to receive the output tokens + address recipient; + /// @dev When emitted on the origin chain, the destination chain for the Output. + /// When emitted on the destination chain, the origin chain for the Order containing the Output. + uint32 chainId; + } +} diff --git a/src/Orders.sol b/src/Orders.sol index aa02e52..a0738b1 100644 --- a/src/Orders.sol +++ b/src/Orders.sol @@ -2,45 +2,30 @@ pragma solidity ^0.8.24; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -/// @notice Tokens sent by the swapper as inputs to the order -/// @dev From ERC-7683 -struct Input { - /// @dev The address of the ERC20 token on the origin chain - address token; - /// @dev The amount of the token to be sent - uint256 amount; -} - -/// @notice Tokens that must be receive for a valid order fulfillment -/// @dev From ERC-7683 -struct Output { - /// @dev The address of the ERC20 token on the destination chain - /// @dev address(0) used as a sentinel for the native token - address token; - /// @dev The amount of the token to be sent - uint256 amount; - /// @dev The address to receive the output tokens - address recipient; - /// @dev When emitted on the origin chain, the destination chain for the Output. - /// When emitted on the destination chain, the origin chain for the Order containing the Output. - uint32 chainId; -} +import {Permit2Batch, UsesPermit2} from "./UsesPermit2.sol"; +import {IOrders} from "./IOrders.sol"; /// @notice Contract capable of processing fulfillment of intent-based Orders. -abstract contract OrderDestination { +abstract contract OrderDestination is IOrders, UsesPermit2 { /// @notice Emitted when Order Outputs are sent to their recipients. /// @dev NOTE that here, Output.chainId denotes the *origin* chainId. event Filled(Output[] outputs); - /// @notice Send the Output(s) of any number of Orders. - /// The user calls `initiate` on a rollup; the Builder calls `fill` on the target chain aggregating Outputs. - /// Builder may aggregate multiple Outputs with the same (`chainId`, `recipient`, `token`) into a single Output with the summed `amount`. + /// @notice Fill any number of Order(s), by transferring their Output(s). + /// @dev Filler may aggregate multiple Outputs with the same (`chainId`, `recipient`, `token`) into a single Output with the summed `amount`. /// @dev NOTE that here, Output.chainId denotes the *origin* chainId. /// @param outputs - The Outputs to be transferred. /// @custom:emits Filled function fill(Output[] memory outputs) external payable { // transfer outputs + _transferOutputs(outputs); + + // emit + emit Filled(outputs); + } + + /// @notice Transfer the Order Outputs to their recipients. + function _transferOutputs(Output[] memory outputs) internal { uint256 value = msg.value; for (uint256 i; i < outputs.length; i++) { if (outputs[i].token == address(0)) { @@ -51,19 +36,32 @@ abstract contract OrderDestination { IERC20(outputs[i].token).transferFrom(msg.sender, outputs[i].recipient, outputs[i].amount); } } + } + + /// @notice Fill any number of Order(s), by transferring their Output(s) via permit2 signed batch transfer. + /// @dev Can only provide ERC20 tokens as Outputs. + /// @dev Filler may aggregate multiple Outputs with the same (`chainId`, `recipient`, `token`) into a single Output with the summed `amount`. + /// @dev the permit2 signer is the Filler providing the Outputs. + /// @dev the permit2 `permitted` tokens MUST match provided Outputs. + /// @dev Filler MUST submit `fill` and `intitiate` within an atomic bundle. + /// @dev NOTE that here, Output.chainId denotes the *origin* chainId. + /// @param outputs - The Outputs to be transferred. signed over via permit2 witness. + /// @param permit2 - the permit2 details, signer, and signature. + /// @custom:emits Filled + function fillPermit2(Output[] memory outputs, Permit2Batch calldata permit2) external { + // transfer all tokens to the Output recipients via permit2 (includes check on nonce & deadline) + _permitWitnessTransferFrom(outputs, _fillTransferDetails(outputs, permit2.permit.permitted), permit2); + // emit emit Filled(outputs); } } /// @notice Contract capable of registering initiation of intent-based Orders. -abstract contract OrderOrigin { +abstract contract OrderOrigin is IOrders, UsesPermit2 { /// @notice Thrown when an Order is submitted with a deadline that has passed. error OrderExpired(); - /// @notice Thrown when trying to call `sweep` if not the Builder of the block. - error OnlyBuilder(); - /// @notice Emitted when an Order is submitted for fulfillment. /// @dev NOTE that here, Output.chainId denotes the *destination* chainId. event Order(uint256 deadline, Input[] inputs, Output[] outputs); @@ -73,14 +71,15 @@ abstract contract OrderOrigin { /// Intentionally does not bother to emit which token(s) were swept, nor their amounts. event Sweep(address indexed recipient, address indexed token, uint256 amount); - /// @notice Request to swap ERC20s. + /// @notice Initiate an Order. + /// @dev Filler MUST submit `fill` and `intitiate` + `sweep` within an atomic bundle. + /// @dev NOTE that here, Output.chainId denotes the *target* chainId. /// @dev inputs are provided on the rollup; in exchange, /// outputs are expected to be received on the target chain(s). - /// @dev Fees paid to the Builders for fulfilling the Orders - /// can be included within the "exchange rate" between inputs and outputs. - /// @dev The Builder claims the inputs from the contract by submitting `sweep` transactions within the same block. /// @dev The Rollup STF MUST NOT apply `initiate` transactions to the rollup state /// UNLESS the outputs are delivered on the target chains within the same block. + /// @dev Fees paid to the Builders for fulfilling the Orders + /// can be included within the "exchange rate" between inputs and outputs. /// @param deadline - The deadline at or before which the Order must be fulfilled. /// @param inputs - The token amounts offered by the swapper in exchange for the outputs. /// @param outputs - The token amounts that must be received on their target chain(s) in order for the Order to be executed. @@ -97,7 +96,7 @@ abstract contract OrderOrigin { emit Order(deadline, inputs, outputs); } - /// @notice Transfer the Order inputs to this contract, where they can be collected by the Order filler. + /// @notice Transfer the Order inputs to this contract, where they can be collected by the Order filler via `sweep`. function _transferInputs(Input[] memory inputs) internal { uint256 value = msg.value; for (uint256 i; i < inputs.length; i++) { @@ -110,6 +109,22 @@ abstract contract OrderOrigin { } } + /// @notice Initiate an Order, transferring Input tokens to the Filler via permit2 signed batch transfer. + /// @dev Can only provide ERC20 tokens as Inputs. + /// @dev the permit2 signer is the swapper providing the Input tokens in exchange for the Outputs. + /// @dev Filler MUST submit `fill` and `intitiate` within an atomic bundle. + /// @dev NOTE that here, Output.chainId denotes the *target* chainId. + /// @param tokenRecipient - the recipient of the Input tokens, provided by msg.sender (un-verified by permit2). + /// @param outputs - the Outputs required in exchange for the Input tokens. signed over via permit2 witness. + /// @param permit2 - the permit2 details, signer, and signature. + function initiatePermit2(address tokenRecipient, Output[] memory outputs, Permit2Batch calldata permit2) external { + // transfer all tokens to the tokenRecipient via permit2 (includes check on nonce & deadline) + _permitWitnessTransferFrom(outputs, _initiateTransferDetails(tokenRecipient, permit2.permit.permitted), permit2); + + // emit + emit Order(permit2.permit.deadline, _inputs(permit2.permit.permitted), outputs); + } + /// @notice Transfer the entire balance of ERC20 tokens to the recipient. /// @dev Called by the Builder within the same block as users' `initiate` transactions /// to claim the `inputs`. @@ -119,7 +134,6 @@ abstract contract OrderOrigin { /// @custom:emits Sweep /// @custom:reverts OnlyBuilder if called by non-block builder function sweep(address recipient, address token) public { - if (msg.sender != block.coinbase) revert OnlyBuilder(); // send ETH or tokens uint256 balance; if (token == address(0)) { @@ -133,6 +147,10 @@ abstract contract OrderOrigin { } } -contract HostOrders is OrderDestination {} +contract HostOrders is OrderDestination { + constructor(address _permit2) UsesPermit2(_permit2) {} +} -contract RollupOrders is OrderOrigin, OrderDestination {} +contract RollupOrders is OrderOrigin, OrderDestination { + constructor(address _permit2) UsesPermit2(_permit2) {} +} diff --git a/src/Passage.sol b/src/Passage.sol index 3077559..05ce890 100644 --- a/src/Passage.sol +++ b/src/Passage.sol @@ -3,9 +3,10 @@ pragma solidity ^0.8.24; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {UsesPermit2, Permit2} from "./UsesPermit2.sol"; /// @notice A contract deployed to Host chain that allows tokens to enter the rollup. -contract Passage { +contract Passage is UsesPermit2 { /// @notice The chainId of rollup that Ether will be sent to by default when entering the rollup via fallback() or receive(). uint256 public immutable defaultRollupChainId; @@ -44,7 +45,12 @@ contract Passage { /// @param _defaultRollupChainId - the chainId of the rollup that Ether will be sent to by default /// when entering the rollup via fallback() or receive() fns. - constructor(uint256 _defaultRollupChainId, address _tokenAdmin, address[] memory initialEnterTokens) { + constructor( + uint256 _defaultRollupChainId, + address _tokenAdmin, + address[] memory initialEnterTokens, + address _permit2 + ) UsesPermit2(_permit2) { defaultRollupChainId = _defaultRollupChainId; tokenAdmin = _tokenAdmin; for (uint256 i; i < initialEnterTokens.length; i++) { @@ -83,10 +89,10 @@ contract Passage { /// @param token - The host chain address of the token entering the rollup. /// @param amount - The amount of tokens entering the rollup. function enterToken(uint256 rollupChainId, address rollupRecipient, address token, uint256 amount) public { - if (!canEnter[token]) revert DisallowedEnter(token); - if (amount == 0) return; + // transfer tokens to this contract IERC20(token).transferFrom(msg.sender, address(this), amount); - emit EnterToken(rollupChainId, rollupRecipient, token, amount); + // check and emit + _enterToken(rollupChainId, rollupRecipient, token, amount); } /// @notice Allows ERC20 tokens to enter the default rollup. @@ -95,6 +101,24 @@ contract Passage { enterToken(defaultRollupChainId, rollupRecipient, token, amount); } + /// @notice Allows ERC20 tokens to enter the rollup. + /// @param rollupChainId - The rollup chain to enter. + /// @param rollupRecipient - The recipient of tokens on the rollup. + /// @param permit2 - The Permit2 information, including token & amount. + function enterTokenPermit2(uint256 rollupChainId, address rollupRecipient, Permit2 calldata permit2) public { + // transfer tokens to this contract via permit2 + _permitWitnessTransferFrom(_witness(rollupChainId, rollupRecipient), permit2); + // check and emit + _enterToken(rollupChainId, rollupRecipient, permit2.permit.permitted.token, permit2.permit.permitted.amount); + } + + /// @notice Shared functionality for tokens entering rollup. + function _enterToken(uint256 rollupChainId, address rollupRecipient, address token, uint256 amount) internal { + if (amount == 0) return; + if (!canEnter[token]) revert DisallowedEnter(token); + emit EnterToken(rollupChainId, rollupRecipient, token, amount); + } + /// @notice Alow/Disallow a given ERC20 token to enter the rollup. function configureEnter(address token, bool _canEnter) external { if (msg.sender != tokenAdmin) revert OnlyTokenAdmin(); @@ -121,7 +145,7 @@ contract Passage { } /// @notice Enables tokens to Exit the rollup. -contract RollupPassage { +contract RollupPassage is UsesPermit2 { /// @notice Emitted when native Ether exits the rollup. /// @param hostRecipient - The *requested* recipient of tokens on the host chain. /// @param amount - The amount of Ether exiting the rollup. @@ -133,6 +157,8 @@ contract RollupPassage { /// @param amount - The amount of ERC20s exiting the rollup. event ExitToken(address indexed hostRecipient, address indexed token, uint256 amount); + constructor(address _permit2) UsesPermit2(_permit2) {} + /// @notice Allows native Ether to exit the rollup by being sent directly to the contract. fallback() external payable { exit(msg.sender); @@ -155,9 +181,28 @@ contract RollupPassage { /// @param hostRecipient - The *requested* recipient of tokens on the host chain. /// @param token - The rollup address of the token exiting the rollup. /// @param amount - The amount of tokens exiting the rollup. + /// @custom:emits ExitToken function exitToken(address hostRecipient, address token, uint256 amount) public { - if (amount == 0) return; + // transfer tokens to this contract IERC20(token).transferFrom(msg.sender, address(this), amount); + // burn and emit + _exitToken(hostRecipient, token, amount); + } + + /// @notice Allows ERC20 tokens to exit the rollup. + /// @param hostRecipient - The *requested* recipient of tokens on the host chain. + /// @param permit2 - The Permit2 information, including token & amount. + /// @custom:emits ExitToken + function exitTokenPermit2(address hostRecipient, Permit2 calldata permit2) public { + // transfer tokens to this contract + _permitWitnessTransferFrom(_witness(hostRecipient), permit2); + // burn and emit + _exitToken(hostRecipient, permit2.permit.permitted.token, permit2.permit.permitted.amount); + } + + /// @notice Shared functionality for tokens exiting rollup. + function _exitToken(address hostRecipient, address token, uint256 amount) internal { + if (amount == 0) return; ERC20Burnable(token).burn(amount); emit ExitToken(hostRecipient, token, amount); } diff --git a/src/UsesPermit2.sol b/src/UsesPermit2.sol new file mode 100644 index 0000000..ec4a088 --- /dev/null +++ b/src/UsesPermit2.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {ISignatureTransfer} from "./vendored/ISignatureTransfer.sol"; +import {IOrders} from "./IOrders.sol"; + +/// @param permit - the permit2 batch token transfer details. includes a `deadline` and an unordered `nonce`. +/// @param signer - the signer of the permit2 info; the owner of the tokens. +/// @param signature - the signature over the permit + witness. +struct Permit2Batch { + ISignatureTransfer.PermitBatchTransferFrom permit; + address owner; + bytes signature; +} + +/// @param permit - the permit2 single token transfer details. includes a `deadline` and an unordered `nonce`. +/// @param signer - the signer of the permit2 info; the owner of the tokens. +/// @param signature - the signature over the permit + witness. +struct Permit2 { + ISignatureTransfer.PermitTransferFrom permit; + address owner; + bytes signature; +} + +abstract contract UsesPermit2 { + string constant _OUTPUT_WITNESS_TYPESTRING = + "Output[] outputs)Output(address token,uint256 amount,address recipient,uint32 chainId)TokenPermissions(address token,uint256 amount)"; + + bytes32 constant _OUTPUT_TYPEHASH = + keccak256("Output(address token,uint256 amount,address recipient,uint32 chainId)"); + + string constant _WITNESS_TYPESTRING = "bytes32 witness)TokenPermissions(address token,uint256 amount)"; + + /// @notice Thrown when a signed Output does not match the corresponding TokenPermissions. + error OutputMismatch(); + + /// @notice The Permit2 contract address. + address immutable permit2Contract; + + constructor(address _permit2) { + permit2Contract = _permit2; + } + + /// @notice Transfer a batch of tokens using permit2. + /// @param outputs - the Outputs for the witness field. + /// @param transferDetails - the TokenPermissions for the transfer, generated based on the use-case (see `_initiateTransferDetails` and `_fillTransferDetails`). + /// @param permit2 - the Permit2Batch information. + function _permitWitnessTransferFrom( + IOrders.Output[] memory outputs, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, + Permit2Batch calldata permit2 + ) internal { + ISignatureTransfer(permit2Contract).permitWitnessTransferFrom( + permit2.permit, + transferDetails, + permit2.owner, + _witness(outputs), + _OUTPUT_WITNESS_TYPESTRING, + permit2.signature + ); + } + + /// @notice Transfer tokens using permit2. + /// @param witness - the pre-hashed witness field. + /// @param permit2 - the Permit2 information. + function _permitWitnessTransferFrom(bytes32 witness, Permit2 calldata permit2) internal { + ISignatureTransfer(permit2Contract).permitWitnessTransferFrom( + permit2.permit, + _passageTransferDetails(permit2.permit.permitted), + permit2.owner, + witness, + _WITNESS_TYPESTRING, + permit2.signature + ); + } + + /// @notice Encode the Output array according to EIP-712 for use as a permit2 witness. + /// @param outputs - the Outputs to encode. + /// @return witness - the encoded witness field. + function _witness(IOrders.Output[] memory outputs) internal pure returns (bytes32 witness) { + uint256 num = outputs.length; + bytes32[] memory hashes = new bytes32[](num); + for (uint256 i = 0; i < num; ++i) { + hashes[i] = keccak256(abi.encode(_OUTPUT_TYPEHASH, outputs[i])); + } + witness = keccak256(abi.encodePacked(hashes)); + } + + /// @notice Encode & hash the rollupChainId and rollupRecipient for use as a permit2 witness. + /// @return witness - the encoded witness field. + function _witness(uint256 rollupChainId, address rollupRecipient) internal pure returns (bytes32 witness) { + witness = keccak256(abi.encode(rollupChainId, rollupRecipient)); + } + + /// @notice Hash the hostRecipient for use as a permit2 witness. + /// @return witness - the encoded witness field. + function _witness(address hostRecipient) internal pure returns (bytes32 witness) { + witness = keccak256(abi.encode(hostRecipient)); + } + + /// @notice transform Output and TokenPermissions structs to TransferDetails structs, for passing to permit2. + /// @dev always transfers the full permitted amount. + /// @param outputs - the Outputs to transform. + /// @param permitted - the TokenPermissions to transform. + /// @return transferDetails - the SignatureTransferDetails generated. + function _fillTransferDetails( + IOrders.Output[] memory outputs, + ISignatureTransfer.TokenPermissions[] calldata permitted + ) internal pure returns (ISignatureTransfer.SignatureTransferDetails[] memory transferDetails) { + if (permitted.length != outputs.length) revert ISignatureTransfer.LengthMismatch(); + transferDetails = new ISignatureTransfer.SignatureTransferDetails[](permitted.length); + for (uint256 i; i < permitted.length; i++) { + if (permitted[i].token != outputs[i].token) revert OutputMismatch(); + if (permitted[i].amount != outputs[i].amount) revert OutputMismatch(); + transferDetails[i] = ISignatureTransfer.SignatureTransferDetails(outputs[i].recipient, outputs[i].amount); + } + } + + /// @notice transform TokenPermissions structs to TransferDetails structs, for passing to permit2. + /// @dev always transfers the full permitted amount. + /// @param tokenRecipient - recipient of all the permitted tokens. + /// @param permitted - the TokenPermissions to transform. + /// @return transferDetails - the SignatureTransferDetails generated. + function _initiateTransferDetails(address tokenRecipient, ISignatureTransfer.TokenPermissions[] calldata permitted) + internal + pure + returns (ISignatureTransfer.SignatureTransferDetails[] memory transferDetails) + { + transferDetails = new ISignatureTransfer.SignatureTransferDetails[](permitted.length); + for (uint256 i; i < permitted.length; i++) { + transferDetails[i] = ISignatureTransfer.SignatureTransferDetails(tokenRecipient, permitted[i].amount); + } + } + + /// @notice transform TokenPermissions to TransferDetails, for passing to permit2. + /// @dev always transfers the full permitted amount to address(this). + /// @param permitted - the TokenPermissions to transform. + /// @return transferDetails - the SignatureTransferDetails generated. + function _passageTransferDetails(ISignatureTransfer.TokenPermissions calldata permitted) + internal + view + returns (ISignatureTransfer.SignatureTransferDetails memory transferDetails) + { + transferDetails.to = address(this); + transferDetails.requestedAmount = permitted.amount; + } + + /// @notice transform permit2 TokenPermissions to Inputs structs, for emitting. + /// @dev TokenPermissions and Inputs structs contain identical fields - (address token, uint256 amount). + /// @param permitted - the TokenPermissions to transform. + /// @return inputs - the Inputs generated. + function _inputs(ISignatureTransfer.TokenPermissions[] calldata permitted) + internal + pure + returns (IOrders.Input[] memory inputs) + { + inputs = new IOrders.Input[](permitted.length); + for (uint256 i; i < permitted.length; i++) { + inputs[i] = IOrders.Input(permitted[i].token, permitted[i].amount); + } + } +} diff --git a/src/vendored/IEIP712.sol b/src/vendored/IEIP712.sol new file mode 100644 index 0000000..48c5c0f --- /dev/null +++ b/src/vendored/IEIP712.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IEIP712 { + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/src/vendored/ISignatureTransfer.sol b/src/vendored/ISignatureTransfer.sol new file mode 100644 index 0000000..71ccb8e --- /dev/null +++ b/src/vendored/ISignatureTransfer.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IEIP712} from "./IEIP712.sol"; + +/// @title SignatureTransfer +/// @notice Handles ERC20 token transfers through signature based actions +/// @dev Requires user's token approval on the Permit2 contract +interface ISignatureTransfer is IEIP712 { + /// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount + /// @param maxAmount The maximum amount a spender can request to transfer + error InvalidAmount(uint256 maxAmount); + + /// @notice Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred + /// @dev If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be transferred + error LengthMismatch(); + + /// @notice Emits an event when the owner successfully invalidates an unordered nonce. + event UnorderedNonceInvalidation(address indexed owner, uint256 word, uint256 mask); + + /// @notice The token and amount details for a transfer signed in the permit transfer signature + struct TokenPermissions { + // ERC20 token address + address token; + // the maximum amount that can be spent + uint256 amount; + } + + /// @notice The signed permit message for a single token transfer + struct PermitTransferFrom { + TokenPermissions permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /// @notice Specifies the recipient address and amount for batched transfers. + /// @dev Recipients and amounts correspond to the index of the signed token permissions array. + /// @dev Reverts if the requested amount is greater than the permitted signed amount. + struct SignatureTransferDetails { + // recipient address + address to; + // spender requested amount + uint256 requestedAmount; + } + + /// @notice Used to reconstruct the signed permit message for multiple token transfers + /// @dev Do not need to pass in spender address as it is required that it is msg.sender + /// @dev Note that a user still signs over a spender address + struct PermitBatchTransferFrom { + // the tokens and corresponding amounts permitted for a transfer + TokenPermissions[] permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /// @notice A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection + /// @dev Uses unordered nonces so that permit messages do not need to be spent in a certain order + /// @dev The mapping is indexed first by the token owner, then by an index specified in the nonce + /// @dev It returns a uint256 bitmap + /// @dev The index, or wordPosition is capped at type(uint248).max + function nonceBitmap(address, uint256) external view returns (uint256); + + /// @notice Transfers a token using a signed permit message + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param signature The signature to verify + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers a token using a signed permit message + /// @notice Includes extra data provided by the caller to verify signature over + /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param witness Extra data to include when checking the user signature + /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash + /// @param signature The signature to verify + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails Specifies the recipient and requested amount for the token transfer + /// @param signature The signature to verify + function permitTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message + /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition + /// @notice Includes extra data provided by the caller to verify signature over + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails Specifies the recipient and requested amount for the token transfer + /// @param witness Extra data to include when checking the user signature + /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash + /// @param signature The signature to verify + function permitWitnessTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Invalidates the bits specified in mask for the bitmap at the word position + /// @dev The wordPos is maxed at type(uint248).max + /// @param wordPos A number to index the nonceBitmap at + /// @param mask A bitmap masked against msg.sender's current bitmap at the word position + function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external; +} diff --git a/test/Orders.t.sol b/test/Orders.t.sol index 64f4a7c..3945f46 100644 --- a/test/Orders.t.sol +++ b/test/Orders.t.sol @@ -2,14 +2,16 @@ pragma solidity ^0.8.24; import {Test, console2} from "forge-std/Test.sol"; -import {RollupOrders, Input, Output, OrderOrigin} from "../src/Orders.sol"; import {TestERC20} from "./Helpers.t.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {RollupOrders, OrderOrigin} from "../src/Orders.sol"; +import {IOrders} from "../src/IOrders.sol"; + contract OrdersTest is Test { RollupOrders public target; - Input[] public inputs; - Output[] public outputs; + IOrders.Input[] public inputs; + IOrders.Output[] public outputs; mapping(address => bool) isToken; @@ -19,14 +21,14 @@ contract OrdersTest is Test { uint256 amount = 200; uint256 deadline = block.timestamp; - event Filled(Output[] outputs); + event Filled(IOrders.Output[] outputs); - event Order(uint256 deadline, Input[] inputs, Output[] outputs); + event Order(uint256 deadline, IOrders.Input[] inputs, IOrders.Output[] outputs); event Sweep(address indexed recipient, address indexed token, uint256 amount); function setUp() public { - target = new RollupOrders(); + target = new RollupOrders(address(0)); // setup token token = address(new TestERC20("hi", "HI")); @@ -35,10 +37,10 @@ contract OrdersTest is Test { isToken[token] = true; // setup Order Inputs/Outputs - Input memory input = Input(token, amount); + IOrders.Input memory input = IOrders.Input(token, amount); inputs.push(input); - Output memory output = Output(token, amount, recipient, chainId); + IOrders.Output memory output = IOrders.Output(token, amount, recipient, chainId); outputs.push(output); } @@ -70,7 +72,7 @@ contract OrdersTest is Test { // input ETH and ERC20 function test_initiate_both() public { // add ETH input - inputs.push(Input(address(0), amount)); + inputs.push(IOrders.Input(address(0), amount)); // expect Order event is initiated, ERC20 is transferred vm.expectEmit(); @@ -92,7 +94,7 @@ contract OrdersTest is Test { TestERC20(token2).approve(address(target), amount * 10000); // add second token input - inputs.push(Input(token2, amount * 2)); + inputs.push(IOrders.Input(token2, amount * 2)); // expect Order event is initiated, ERC20 is transferred vm.expectEmit(); @@ -111,7 +113,7 @@ contract OrdersTest is Test { // change first input to ETH inputs[0].token = address(0); // add second ETH input - inputs.push(Input(address(0), amount * 2)); + inputs.push(IOrders.Input(address(0), amount * 2)); // expect Order event is initiated vm.expectEmit(); @@ -126,7 +128,7 @@ contract OrdersTest is Test { // change first input to ETH inputs[0].token = address(0); // add second ETH input - inputs.push(Input(address(0), 1)); + inputs.push(IOrders.Input(address(0), 1)); // total ETH inputs should be amount + 1; function should underflow only sending amount vm.expectRevert(); @@ -172,11 +174,6 @@ contract OrdersTest is Test { target.sweep(recipient, token); } - function test_onlyBuilder() public { - vm.expectRevert(OrderOrigin.OnlyBuilder.selector); - target.sweep(recipient, token); - } - function test_fill_ETH() public { outputs[0].token = address(0); @@ -197,7 +194,7 @@ contract OrdersTest is Test { function test_fill_both() public { // add ETH output - outputs.push(Output(address(0), amount * 2, recipient, chainId)); + outputs.push(IOrders.Output(address(0), amount * 2, recipient, chainId)); // expect Outputs are filled, ERC20 is transferred vm.expectEmit(); @@ -214,7 +211,7 @@ contract OrdersTest is Test { // change first output to ETH outputs[0].token = address(0); // add second ETH oputput - outputs.push(Output(address(0), amount * 2, recipient, chainId)); + outputs.push(IOrders.Output(address(0), amount * 2, recipient, chainId)); // expect Order event is initiated vm.expectEmit(); @@ -229,7 +226,7 @@ contract OrdersTest is Test { // change first output to ETH outputs[0].token = address(0); // add second ETH output - outputs.push(Output(address(0), 1, recipient, chainId)); + outputs.push(IOrders.Output(address(0), 1, recipient, chainId)); // total ETH outputs should be `amount` + 1; function should underflow only sending `amount` vm.expectRevert(); diff --git a/test/Passage.t.sol b/test/Passage.t.sol index cf04dc8..1c682b0 100644 --- a/test/Passage.t.sol +++ b/test/Passage.t.sol @@ -51,7 +51,7 @@ contract PassageTest is Test { // deploy target address[] memory initialEnterTokens = new address[](1); initialEnterTokens[0] = token; - target = new Passage(block.chainid + 1, address(this), initialEnterTokens); + target = new Passage(block.chainid + 1, address(this), initialEnterTokens, address(0)); TestERC20(token).approve(address(target), amount * 10000); // deploy token two, don't configure @@ -173,7 +173,7 @@ contract RollupPassageTest is Test { function setUp() public { // deploy target - target = new RollupPassage(); + target = new RollupPassage(address(0)); // deploy token token = address(new TestERC20("hi", "HI")); diff --git a/test/Transact.t.sol b/test/Transact.t.sol index f875092..673f72c 100644 --- a/test/Transact.t.sol +++ b/test/Transact.t.sol @@ -35,7 +35,7 @@ contract TransactTest is Test { function setUp() public { // deploy target - passage = new Passage(block.chainid + 1, address(this), new address[](0)); + passage = new Passage(block.chainid + 1, address(this), new address[](0), address(0)); target = new Transactor(block.chainid + 1, address(this), passage, gas * 6, gas); } From 1de94f552278804832b649ab86e968ea7e9ba7e5 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Thu, 18 Jul 2024 15:32:48 +0200 Subject: [PATCH 02/18] add permit2 as submodule, organize files to folders (#65) * forge install: permit2 * remove vendored contracts * move IOrders to interfaces/ * move permit2 to folder --- .gitmodules | 3 + lib/permit2 | 1 + src/Orders.sol | 4 +- src/Passage.sol | 2 +- src/Zenith.sol | 2 - src/{ => interfaces}/IOrders.sol | 0 src/{ => permit2}/UsesPermit2.sol | 4 +- src/vendored/IEIP712.sol | 6 -- src/vendored/ISignatureTransfer.sol | 134 ---------------------------- test/Orders.t.sol | 3 +- 10 files changed, 10 insertions(+), 149 deletions(-) create mode 160000 lib/permit2 rename src/{ => interfaces}/IOrders.sol (100%) rename src/{ => permit2}/UsesPermit2.sol (98%) delete mode 100644 src/vendored/IEIP712.sol delete mode 100644 src/vendored/ISignatureTransfer.sol diff --git a/.gitmodules b/.gitmodules index 690924b..937a6ea 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/permit2"] + path = lib/permit2 + url = https://github.com/Uniswap/permit2 diff --git a/lib/permit2 b/lib/permit2 new file mode 160000 index 0000000..cc56ad0 --- /dev/null +++ b/lib/permit2 @@ -0,0 +1 @@ +Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219 diff --git a/src/Orders.sol b/src/Orders.sol index a0738b1..3cf3d1e 100644 --- a/src/Orders.sol +++ b/src/Orders.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; +import {UsesPermit2, Permit2Batch} from "./permit2/UsesPermit2.sol"; +import {IOrders} from "./interfaces/IOrders.sol"; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {Permit2Batch, UsesPermit2} from "./UsesPermit2.sol"; -import {IOrders} from "./IOrders.sol"; /// @notice Contract capable of processing fulfillment of intent-based Orders. abstract contract OrderDestination is IOrders, UsesPermit2 { diff --git a/src/Passage.sol b/src/Passage.sol index 05ce890..4abc900 100644 --- a/src/Passage.sol +++ b/src/Passage.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; +import {UsesPermit2, Permit2} from "./permit2/UsesPermit2.sol"; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; -import {UsesPermit2, Permit2} from "./UsesPermit2.sol"; /// @notice A contract deployed to Host chain that allows tokens to enter the rollup. contract Passage is UsesPermit2 { diff --git a/src/Zenith.sol b/src/Zenith.sol index 6bcb1bf..7336062 100644 --- a/src/Zenith.sol +++ b/src/Zenith.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {Passage} from "./Passage.sol"; - contract Zenith { /// @notice The address that is allowed to set/remove sequencers. address public immutable sequencerAdmin; diff --git a/src/IOrders.sol b/src/interfaces/IOrders.sol similarity index 100% rename from src/IOrders.sol rename to src/interfaces/IOrders.sol diff --git a/src/UsesPermit2.sol b/src/permit2/UsesPermit2.sol similarity index 98% rename from src/UsesPermit2.sol rename to src/permit2/UsesPermit2.sol index ec4a088..956a495 100644 --- a/src/UsesPermit2.sol +++ b/src/permit2/UsesPermit2.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {ISignatureTransfer} from "./vendored/ISignatureTransfer.sol"; -import {IOrders} from "./IOrders.sol"; +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; +import {IOrders} from "../interfaces/IOrders.sol"; /// @param permit - the permit2 batch token transfer details. includes a `deadline` and an unordered `nonce`. /// @param signer - the signer of the permit2 info; the owner of the tokens. diff --git a/src/vendored/IEIP712.sol b/src/vendored/IEIP712.sol deleted file mode 100644 index 48c5c0f..0000000 --- a/src/vendored/IEIP712.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -interface IEIP712 { - function DOMAIN_SEPARATOR() external view returns (bytes32); -} diff --git a/src/vendored/ISignatureTransfer.sol b/src/vendored/ISignatureTransfer.sol deleted file mode 100644 index 71ccb8e..0000000 --- a/src/vendored/ISignatureTransfer.sol +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {IEIP712} from "./IEIP712.sol"; - -/// @title SignatureTransfer -/// @notice Handles ERC20 token transfers through signature based actions -/// @dev Requires user's token approval on the Permit2 contract -interface ISignatureTransfer is IEIP712 { - /// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount - /// @param maxAmount The maximum amount a spender can request to transfer - error InvalidAmount(uint256 maxAmount); - - /// @notice Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred - /// @dev If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be transferred - error LengthMismatch(); - - /// @notice Emits an event when the owner successfully invalidates an unordered nonce. - event UnorderedNonceInvalidation(address indexed owner, uint256 word, uint256 mask); - - /// @notice The token and amount details for a transfer signed in the permit transfer signature - struct TokenPermissions { - // ERC20 token address - address token; - // the maximum amount that can be spent - uint256 amount; - } - - /// @notice The signed permit message for a single token transfer - struct PermitTransferFrom { - TokenPermissions permitted; - // a unique value for every token owner's signature to prevent signature replays - uint256 nonce; - // deadline on the permit signature - uint256 deadline; - } - - /// @notice Specifies the recipient address and amount for batched transfers. - /// @dev Recipients and amounts correspond to the index of the signed token permissions array. - /// @dev Reverts if the requested amount is greater than the permitted signed amount. - struct SignatureTransferDetails { - // recipient address - address to; - // spender requested amount - uint256 requestedAmount; - } - - /// @notice Used to reconstruct the signed permit message for multiple token transfers - /// @dev Do not need to pass in spender address as it is required that it is msg.sender - /// @dev Note that a user still signs over a spender address - struct PermitBatchTransferFrom { - // the tokens and corresponding amounts permitted for a transfer - TokenPermissions[] permitted; - // a unique value for every token owner's signature to prevent signature replays - uint256 nonce; - // deadline on the permit signature - uint256 deadline; - } - - /// @notice A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection - /// @dev Uses unordered nonces so that permit messages do not need to be spent in a certain order - /// @dev The mapping is indexed first by the token owner, then by an index specified in the nonce - /// @dev It returns a uint256 bitmap - /// @dev The index, or wordPosition is capped at type(uint248).max - function nonceBitmap(address, uint256) external view returns (uint256); - - /// @notice Transfers a token using a signed permit message - /// @dev Reverts if the requested amount is greater than the permitted signed amount - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails The spender's requested transfer details for the permitted token - /// @param signature The signature to verify - function permitTransferFrom( - PermitTransferFrom memory permit, - SignatureTransferDetails calldata transferDetails, - address owner, - bytes calldata signature - ) external; - - /// @notice Transfers a token using a signed permit message - /// @notice Includes extra data provided by the caller to verify signature over - /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition - /// @dev Reverts if the requested amount is greater than the permitted signed amount - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails The spender's requested transfer details for the permitted token - /// @param witness Extra data to include when checking the user signature - /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash - /// @param signature The signature to verify - function permitWitnessTransferFrom( - PermitTransferFrom memory permit, - SignatureTransferDetails calldata transferDetails, - address owner, - bytes32 witness, - string calldata witnessTypeString, - bytes calldata signature - ) external; - - /// @notice Transfers multiple tokens using a signed permit message - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails Specifies the recipient and requested amount for the token transfer - /// @param signature The signature to verify - function permitTransferFrom( - PermitBatchTransferFrom memory permit, - SignatureTransferDetails[] calldata transferDetails, - address owner, - bytes calldata signature - ) external; - - /// @notice Transfers multiple tokens using a signed permit message - /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition - /// @notice Includes extra data provided by the caller to verify signature over - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails Specifies the recipient and requested amount for the token transfer - /// @param witness Extra data to include when checking the user signature - /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash - /// @param signature The signature to verify - function permitWitnessTransferFrom( - PermitBatchTransferFrom memory permit, - SignatureTransferDetails[] calldata transferDetails, - address owner, - bytes32 witness, - string calldata witnessTypeString, - bytes calldata signature - ) external; - - /// @notice Invalidates the bits specified in mask for the bitmap at the word position - /// @dev The wordPos is maxed at type(uint248).max - /// @param wordPos A number to index the nonceBitmap at - /// @param mask A bitmap masked against msg.sender's current bitmap at the word position - function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external; -} diff --git a/test/Orders.t.sol b/test/Orders.t.sol index 3945f46..00a4046 100644 --- a/test/Orders.t.sol +++ b/test/Orders.t.sol @@ -4,9 +4,8 @@ pragma solidity ^0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {TestERC20} from "./Helpers.t.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; - import {RollupOrders, OrderOrigin} from "../src/Orders.sol"; -import {IOrders} from "../src/IOrders.sol"; +import {IOrders} from "../src/interfaces/IOrders.sol"; contract OrdersTest is Test { RollupOrders public target; From a6f54e4f9e31e13d31586c770330923ea3e01c36 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Thu, 18 Jul 2024 15:01:58 +0100 Subject: [PATCH 03/18] break permit2 functionality into discrete contracts --- src/Orders.sol | 14 ++-- src/Passage.sol | 12 ++-- src/permit2/UsesPermit2.sol | 130 +++++++++++++++++++----------------- 3 files changed, 83 insertions(+), 73 deletions(-) diff --git a/src/Orders.sol b/src/Orders.sol index 3cf3d1e..838d5d8 100644 --- a/src/Orders.sol +++ b/src/Orders.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {UsesPermit2, Permit2Batch} from "./permit2/UsesPermit2.sol"; +import {OrdersPermit2, UsesPermit2} from "./permit2/UsesPermit2.sol"; import {IOrders} from "./interfaces/IOrders.sol"; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; /// @notice Contract capable of processing fulfillment of intent-based Orders. -abstract contract OrderDestination is IOrders, UsesPermit2 { +abstract contract OrderDestination is IOrders, OrdersPermit2 { /// @notice Emitted when Order Outputs are sent to their recipients. /// @dev NOTE that here, Output.chainId denotes the *origin* chainId. event Filled(Output[] outputs); @@ -48,7 +48,7 @@ abstract contract OrderDestination is IOrders, UsesPermit2 { /// @param outputs - The Outputs to be transferred. signed over via permit2 witness. /// @param permit2 - the permit2 details, signer, and signature. /// @custom:emits Filled - function fillPermit2(Output[] memory outputs, Permit2Batch calldata permit2) external { + function fillPermit2(Output[] memory outputs, OrdersPermit2.Permit2Batch calldata permit2) external { // transfer all tokens to the Output recipients via permit2 (includes check on nonce & deadline) _permitWitnessTransferFrom(outputs, _fillTransferDetails(outputs, permit2.permit.permitted), permit2); @@ -58,7 +58,7 @@ abstract contract OrderDestination is IOrders, UsesPermit2 { } /// @notice Contract capable of registering initiation of intent-based Orders. -abstract contract OrderOrigin is IOrders, UsesPermit2 { +abstract contract OrderOrigin is IOrders, OrdersPermit2 { /// @notice Thrown when an Order is submitted with a deadline that has passed. error OrderExpired(); @@ -117,7 +117,11 @@ abstract contract OrderOrigin is IOrders, UsesPermit2 { /// @param tokenRecipient - the recipient of the Input tokens, provided by msg.sender (un-verified by permit2). /// @param outputs - the Outputs required in exchange for the Input tokens. signed over via permit2 witness. /// @param permit2 - the permit2 details, signer, and signature. - function initiatePermit2(address tokenRecipient, Output[] memory outputs, Permit2Batch calldata permit2) external { + function initiatePermit2( + address tokenRecipient, + Output[] memory outputs, + OrdersPermit2.Permit2Batch calldata permit2 + ) external { // transfer all tokens to the tokenRecipient via permit2 (includes check on nonce & deadline) _permitWitnessTransferFrom(outputs, _initiateTransferDetails(tokenRecipient, permit2.permit.permitted), permit2); diff --git a/src/Passage.sol b/src/Passage.sol index 4abc900..87bbc04 100644 --- a/src/Passage.sol +++ b/src/Passage.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {UsesPermit2, Permit2} from "./permit2/UsesPermit2.sol"; +import {PassagePermit2, UsesPermit2} from "./permit2/UsesPermit2.sol"; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; /// @notice A contract deployed to Host chain that allows tokens to enter the rollup. -contract Passage is UsesPermit2 { +contract Passage is PassagePermit2 { /// @notice The chainId of rollup that Ether will be sent to by default when entering the rollup via fallback() or receive(). uint256 public immutable defaultRollupChainId; @@ -105,7 +105,9 @@ contract Passage is UsesPermit2 { /// @param rollupChainId - The rollup chain to enter. /// @param rollupRecipient - The recipient of tokens on the rollup. /// @param permit2 - The Permit2 information, including token & amount. - function enterTokenPermit2(uint256 rollupChainId, address rollupRecipient, Permit2 calldata permit2) public { + function enterTokenPermit2(uint256 rollupChainId, address rollupRecipient, PassagePermit2.Permit2 calldata permit2) + public + { // transfer tokens to this contract via permit2 _permitWitnessTransferFrom(_witness(rollupChainId, rollupRecipient), permit2); // check and emit @@ -145,7 +147,7 @@ contract Passage is UsesPermit2 { } /// @notice Enables tokens to Exit the rollup. -contract RollupPassage is UsesPermit2 { +contract RollupPassage is PassagePermit2 { /// @notice Emitted when native Ether exits the rollup. /// @param hostRecipient - The *requested* recipient of tokens on the host chain. /// @param amount - The amount of Ether exiting the rollup. @@ -193,7 +195,7 @@ contract RollupPassage is UsesPermit2 { /// @param hostRecipient - The *requested* recipient of tokens on the host chain. /// @param permit2 - The Permit2 information, including token & amount. /// @custom:emits ExitToken - function exitTokenPermit2(address hostRecipient, Permit2 calldata permit2) public { + function exitTokenPermit2(address hostRecipient, PassagePermit2.Permit2 calldata permit2) public { // transfer tokens to this contract _permitWitnessTransferFrom(_witness(hostRecipient), permit2); // burn and emit diff --git a/src/permit2/UsesPermit2.sol b/src/permit2/UsesPermit2.sol index 956a495..4da5779 100644 --- a/src/permit2/UsesPermit2.sol +++ b/src/permit2/UsesPermit2.sol @@ -4,43 +4,34 @@ pragma solidity ^0.8.24; import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; import {IOrders} from "../interfaces/IOrders.sol"; -/// @param permit - the permit2 batch token transfer details. includes a `deadline` and an unordered `nonce`. -/// @param signer - the signer of the permit2 info; the owner of the tokens. -/// @param signature - the signature over the permit + witness. -struct Permit2Batch { - ISignatureTransfer.PermitBatchTransferFrom permit; - address owner; - bytes signature; -} +abstract contract UsesPermit2 { + /// @notice The Permit2 contract address. + address immutable permit2Contract; -/// @param permit - the permit2 single token transfer details. includes a `deadline` and an unordered `nonce`. -/// @param signer - the signer of the permit2 info; the owner of the tokens. -/// @param signature - the signature over the permit + witness. -struct Permit2 { - ISignatureTransfer.PermitTransferFrom permit; - address owner; - bytes signature; + constructor(address _permit2) { + permit2Contract = _permit2; + } } -abstract contract UsesPermit2 { +abstract contract OrdersPermit2 is UsesPermit2 { string constant _OUTPUT_WITNESS_TYPESTRING = "Output[] outputs)Output(address token,uint256 amount,address recipient,uint32 chainId)TokenPermissions(address token,uint256 amount)"; bytes32 constant _OUTPUT_TYPEHASH = keccak256("Output(address token,uint256 amount,address recipient,uint32 chainId)"); - string constant _WITNESS_TYPESTRING = "bytes32 witness)TokenPermissions(address token,uint256 amount)"; + /// @param permit - the permit2 batch token transfer details. includes a `deadline` and an unordered `nonce`. + /// @param signer - the signer of the permit2 info; the owner of the tokens. + /// @param signature - the signature over the permit + witness. + struct Permit2Batch { + ISignatureTransfer.PermitBatchTransferFrom permit; + address owner; + bytes signature; + } /// @notice Thrown when a signed Output does not match the corresponding TokenPermissions. error OutputMismatch(); - /// @notice The Permit2 contract address. - address immutable permit2Contract; - - constructor(address _permit2) { - permit2Contract = _permit2; - } - /// @notice Transfer a batch of tokens using permit2. /// @param outputs - the Outputs for the witness field. /// @param transferDetails - the TokenPermissions for the transfer, generated based on the use-case (see `_initiateTransferDetails` and `_fillTransferDetails`). @@ -60,20 +51,6 @@ abstract contract UsesPermit2 { ); } - /// @notice Transfer tokens using permit2. - /// @param witness - the pre-hashed witness field. - /// @param permit2 - the Permit2 information. - function _permitWitnessTransferFrom(bytes32 witness, Permit2 calldata permit2) internal { - ISignatureTransfer(permit2Contract).permitWitnessTransferFrom( - permit2.permit, - _passageTransferDetails(permit2.permit.permitted), - permit2.owner, - witness, - _WITNESS_TYPESTRING, - permit2.signature - ); - } - /// @notice Encode the Output array according to EIP-712 for use as a permit2 witness. /// @param outputs - the Outputs to encode. /// @return witness - the encoded witness field. @@ -86,18 +63,6 @@ abstract contract UsesPermit2 { witness = keccak256(abi.encodePacked(hashes)); } - /// @notice Encode & hash the rollupChainId and rollupRecipient for use as a permit2 witness. - /// @return witness - the encoded witness field. - function _witness(uint256 rollupChainId, address rollupRecipient) internal pure returns (bytes32 witness) { - witness = keccak256(abi.encode(rollupChainId, rollupRecipient)); - } - - /// @notice Hash the hostRecipient for use as a permit2 witness. - /// @return witness - the encoded witness field. - function _witness(address hostRecipient) internal pure returns (bytes32 witness) { - witness = keccak256(abi.encode(hostRecipient)); - } - /// @notice transform Output and TokenPermissions structs to TransferDetails structs, for passing to permit2. /// @dev always transfers the full permitted amount. /// @param outputs - the Outputs to transform. @@ -132,19 +97,6 @@ abstract contract UsesPermit2 { } } - /// @notice transform TokenPermissions to TransferDetails, for passing to permit2. - /// @dev always transfers the full permitted amount to address(this). - /// @param permitted - the TokenPermissions to transform. - /// @return transferDetails - the SignatureTransferDetails generated. - function _passageTransferDetails(ISignatureTransfer.TokenPermissions calldata permitted) - internal - view - returns (ISignatureTransfer.SignatureTransferDetails memory transferDetails) - { - transferDetails.to = address(this); - transferDetails.requestedAmount = permitted.amount; - } - /// @notice transform permit2 TokenPermissions to Inputs structs, for emitting. /// @dev TokenPermissions and Inputs structs contain identical fields - (address token, uint256 amount). /// @param permitted - the TokenPermissions to transform. @@ -160,3 +112,55 @@ abstract contract UsesPermit2 { } } } + +abstract contract PassagePermit2 is UsesPermit2 { + string constant _WITNESS_TYPESTRING = "bytes32 witness)TokenPermissions(address token,uint256 amount)"; + + /// @param permit - the permit2 single token transfer details. includes a `deadline` and an unordered `nonce`. + /// @param signer - the signer of the permit2 info; the owner of the tokens. + /// @param signature - the signature over the permit + witness. + struct Permit2 { + ISignatureTransfer.PermitTransferFrom permit; + address owner; + bytes signature; + } + + /// @notice Transfer tokens using permit2. + /// @param witness - the pre-hashed witness field. + /// @param permit2 - the Permit2 information. + function _permitWitnessTransferFrom(bytes32 witness, Permit2 calldata permit2) internal { + ISignatureTransfer(permit2Contract).permitWitnessTransferFrom( + permit2.permit, + _passageTransferDetails(permit2.permit.permitted), + permit2.owner, + witness, + _WITNESS_TYPESTRING, + permit2.signature + ); + } + + /// @notice Encode & hash the rollupChainId and rollupRecipient for use as a permit2 witness. + /// @return witness - the encoded witness field. + function _witness(uint256 rollupChainId, address rollupRecipient) internal pure returns (bytes32 witness) { + witness = keccak256(abi.encode(rollupChainId, rollupRecipient)); + } + + /// @notice Hash the hostRecipient for use as a permit2 witness. + /// @return witness - the encoded witness field. + function _witness(address hostRecipient) internal pure returns (bytes32 witness) { + witness = keccak256(abi.encode(hostRecipient)); + } + + /// @notice transform TokenPermissions to TransferDetails, for passing to permit2. + /// @dev always transfers the full permitted amount to address(this). + /// @param permitted - the TokenPermissions to transform. + /// @return transferDetails - the SignatureTransferDetails generated. + function _passageTransferDetails(ISignatureTransfer.TokenPermissions calldata permitted) + internal + view + returns (ISignatureTransfer.SignatureTransferDetails memory transferDetails) + { + transferDetails.to = address(this); + transferDetails.requestedAmount = permitted.amount; + } +} From e88165b9de8a1b4c004c28ba2b45c69087d366cd Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Thu, 18 Jul 2024 15:16:24 +0100 Subject: [PATCH 04/18] fix: update witness encoding for EIP-712 compliance --- src/Passage.sol | 4 ++-- src/permit2/UsesPermit2.sol | 33 ++++++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/Passage.sol b/src/Passage.sol index 87bbc04..c8ed2d1 100644 --- a/src/Passage.sol +++ b/src/Passage.sol @@ -109,7 +109,7 @@ contract Passage is PassagePermit2 { public { // transfer tokens to this contract via permit2 - _permitWitnessTransferFrom(_witness(rollupChainId, rollupRecipient), permit2); + _permitWitnessTransferFrom(_enterWitness(rollupChainId, rollupRecipient), true, permit2); // check and emit _enterToken(rollupChainId, rollupRecipient, permit2.permit.permitted.token, permit2.permit.permitted.amount); } @@ -197,7 +197,7 @@ contract RollupPassage is PassagePermit2 { /// @custom:emits ExitToken function exitTokenPermit2(address hostRecipient, PassagePermit2.Permit2 calldata permit2) public { // transfer tokens to this contract - _permitWitnessTransferFrom(_witness(hostRecipient), permit2); + _permitWitnessTransferFrom(_exitWitness(hostRecipient), false, permit2); // burn and emit _exitToken(hostRecipient, permit2.permit.permitted.token, permit2.permit.permitted.amount); } diff --git a/src/permit2/UsesPermit2.sol b/src/permit2/UsesPermit2.sol index 4da5779..b4edad6 100644 --- a/src/permit2/UsesPermit2.sol +++ b/src/permit2/UsesPermit2.sol @@ -114,7 +114,26 @@ abstract contract OrdersPermit2 is UsesPermit2 { } abstract contract PassagePermit2 is UsesPermit2 { - string constant _WITNESS_TYPESTRING = "bytes32 witness)TokenPermissions(address token,uint256 amount)"; + string constant _ENTER_WITNESS_TYPESTRING = + "EnterWitness witness)EnterWitness(uint256 rollupChainId,address rollupRecipient)TokenPermissions(address token,uint256 amount)"; + + bytes32 constant _ENTER_WITNESS_TYPEHASH = keccak256("EnterWitness(uint256 rollupChainId,address rollupRecipient)"); + + string constant _EXIT_WITNESS_TYPESTRING = + "ExitWitness witness)ExitWitness(address hostRecipient)TokenPermissions(address token,uint256 amount)"; + + bytes32 constant _EXIT_WITNESS_TYPEHASH = keccak256("ExitWitness(address hostRecipient)"); + + /// @notice Struct to hash Enter witness data into a 32-byte witness field, in an EIP-712 compliant way. + struct EnterWitness { + uint256 rollupChainId; + address rollupRecipient; + } + + /// @notice Struct to hash Exit witness data into a 32-byte witness field, in an EIP-712 compliant way. + struct ExitWitness { + address hostRecipient; + } /// @param permit - the permit2 single token transfer details. includes a `deadline` and an unordered `nonce`. /// @param signer - the signer of the permit2 info; the owner of the tokens. @@ -128,27 +147,27 @@ abstract contract PassagePermit2 is UsesPermit2 { /// @notice Transfer tokens using permit2. /// @param witness - the pre-hashed witness field. /// @param permit2 - the Permit2 information. - function _permitWitnessTransferFrom(bytes32 witness, Permit2 calldata permit2) internal { + function _permitWitnessTransferFrom(bytes32 witness, bool isEnter, Permit2 calldata permit2) internal { ISignatureTransfer(permit2Contract).permitWitnessTransferFrom( permit2.permit, _passageTransferDetails(permit2.permit.permitted), permit2.owner, witness, - _WITNESS_TYPESTRING, + isEnter ? _ENTER_WITNESS_TYPESTRING : _EXIT_WITNESS_TYPESTRING, permit2.signature ); } /// @notice Encode & hash the rollupChainId and rollupRecipient for use as a permit2 witness. /// @return witness - the encoded witness field. - function _witness(uint256 rollupChainId, address rollupRecipient) internal pure returns (bytes32 witness) { - witness = keccak256(abi.encode(rollupChainId, rollupRecipient)); + function _enterWitness(uint256 rollupChainId, address rollupRecipient) internal pure returns (bytes32 witness) { + witness = keccak256(abi.encode(_ENTER_WITNESS_TYPEHASH, EnterWitness(rollupChainId, rollupRecipient))); } /// @notice Hash the hostRecipient for use as a permit2 witness. /// @return witness - the encoded witness field. - function _witness(address hostRecipient) internal pure returns (bytes32 witness) { - witness = keccak256(abi.encode(hostRecipient)); + function _exitWitness(address hostRecipient) internal pure returns (bytes32 witness) { + witness = keccak256(abi.encode(_EXIT_WITNESS_TYPEHASH, ExitWitness(hostRecipient))); } /// @notice transform TokenPermissions to TransferDetails, for passing to permit2. From 3ea69d2b7f659727b5d3a059ad43129af94bd4ea Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 15:47:50 +0100 Subject: [PATCH 05/18] refactor: generate witness as public field --- src/Orders.sol | 8 +++++-- src/Passage.sol | 4 ++-- src/permit2/UsesPermit2.sol | 48 ++++++++++++++++++++++++------------- test/Passage.t.sol | 2 -- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/Orders.sol b/src/Orders.sol index 838d5d8..c982875 100644 --- a/src/Orders.sol +++ b/src/Orders.sol @@ -50,7 +50,9 @@ abstract contract OrderDestination is IOrders, OrdersPermit2 { /// @custom:emits Filled function fillPermit2(Output[] memory outputs, OrdersPermit2.Permit2Batch calldata permit2) external { // transfer all tokens to the Output recipients via permit2 (includes check on nonce & deadline) - _permitWitnessTransferFrom(outputs, _fillTransferDetails(outputs, permit2.permit.permitted), permit2); + _permitWitnessTransferFrom( + outputsWitness(outputs), _fillTransferDetails(outputs, permit2.permit.permitted), permit2 + ); // emit emit Filled(outputs); @@ -123,7 +125,9 @@ abstract contract OrderOrigin is IOrders, OrdersPermit2 { OrdersPermit2.Permit2Batch calldata permit2 ) external { // transfer all tokens to the tokenRecipient via permit2 (includes check on nonce & deadline) - _permitWitnessTransferFrom(outputs, _initiateTransferDetails(tokenRecipient, permit2.permit.permitted), permit2); + _permitWitnessTransferFrom( + outputsWitness(outputs), _initiateTransferDetails(tokenRecipient, permit2.permit.permitted), permit2 + ); // emit emit Order(permit2.permit.deadline, _inputs(permit2.permit.permitted), outputs); diff --git a/src/Passage.sol b/src/Passage.sol index c8ed2d1..fc1d1a3 100644 --- a/src/Passage.sol +++ b/src/Passage.sol @@ -109,7 +109,7 @@ contract Passage is PassagePermit2 { public { // transfer tokens to this contract via permit2 - _permitWitnessTransferFrom(_enterWitness(rollupChainId, rollupRecipient), true, permit2); + _permitWitnessTransferFrom(enterWitness(rollupChainId, rollupRecipient), permit2); // check and emit _enterToken(rollupChainId, rollupRecipient, permit2.permit.permitted.token, permit2.permit.permitted.amount); } @@ -197,7 +197,7 @@ contract RollupPassage is PassagePermit2 { /// @custom:emits ExitToken function exitTokenPermit2(address hostRecipient, PassagePermit2.Permit2 calldata permit2) public { // transfer tokens to this contract - _permitWitnessTransferFrom(_exitWitness(hostRecipient), false, permit2); + _permitWitnessTransferFrom(exitWitness(hostRecipient), permit2); // burn and emit _exitToken(hostRecipient, permit2.permit.permitted.token, permit2.permit.permitted.amount); } diff --git a/src/permit2/UsesPermit2.sol b/src/permit2/UsesPermit2.sol index b4edad6..9b4d141 100644 --- a/src/permit2/UsesPermit2.sol +++ b/src/permit2/UsesPermit2.sol @@ -5,6 +5,12 @@ import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol" import {IOrders} from "../interfaces/IOrders.sol"; abstract contract UsesPermit2 { + /// @notice Struct to hold the pre-hashed witness field and the witness type string. + struct Witness { + bytes32 witnessHash; + string witnessTypeString; + } + /// @notice The Permit2 contract address. address immutable permit2Contract; @@ -33,11 +39,11 @@ abstract contract OrdersPermit2 is UsesPermit2 { error OutputMismatch(); /// @notice Transfer a batch of tokens using permit2. - /// @param outputs - the Outputs for the witness field. + /// @param _witness - the hashed witness and its typestring. /// @param transferDetails - the TokenPermissions for the transfer, generated based on the use-case (see `_initiateTransferDetails` and `_fillTransferDetails`). /// @param permit2 - the Permit2Batch information. function _permitWitnessTransferFrom( - IOrders.Output[] memory outputs, + Witness memory _witness, ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, Permit2Batch calldata permit2 ) internal { @@ -45,22 +51,23 @@ abstract contract OrdersPermit2 is UsesPermit2 { permit2.permit, transferDetails, permit2.owner, - _witness(outputs), - _OUTPUT_WITNESS_TYPESTRING, + _witness.witnessHash, + _witness.witnessTypeString, permit2.signature ); } /// @notice Encode the Output array according to EIP-712 for use as a permit2 witness. /// @param outputs - the Outputs to encode. - /// @return witness - the encoded witness field. - function _witness(IOrders.Output[] memory outputs) internal pure returns (bytes32 witness) { + /// @return _witness - the encoded witness field. + function outputsWitness(IOrders.Output[] memory outputs) public pure returns (Witness memory _witness) { uint256 num = outputs.length; bytes32[] memory hashes = new bytes32[](num); for (uint256 i = 0; i < num; ++i) { hashes[i] = keccak256(abi.encode(_OUTPUT_TYPEHASH, outputs[i])); } - witness = keccak256(abi.encodePacked(hashes)); + _witness.witnessHash = keccak256(abi.encodePacked(hashes)); + _witness.witnessTypeString = _OUTPUT_WITNESS_TYPESTRING; } /// @notice transform Output and TokenPermissions structs to TransferDetails structs, for passing to permit2. @@ -145,29 +152,36 @@ abstract contract PassagePermit2 is UsesPermit2 { } /// @notice Transfer tokens using permit2. - /// @param witness - the pre-hashed witness field. + /// @param _witness - the hashed witness and its typestring. /// @param permit2 - the Permit2 information. - function _permitWitnessTransferFrom(bytes32 witness, bool isEnter, Permit2 calldata permit2) internal { + function _permitWitnessTransferFrom(Witness memory _witness, Permit2 calldata permit2) internal { ISignatureTransfer(permit2Contract).permitWitnessTransferFrom( permit2.permit, _passageTransferDetails(permit2.permit.permitted), permit2.owner, - witness, - isEnter ? _ENTER_WITNESS_TYPESTRING : _EXIT_WITNESS_TYPESTRING, + _witness.witnessHash, + _witness.witnessTypeString, permit2.signature ); } /// @notice Encode & hash the rollupChainId and rollupRecipient for use as a permit2 witness. - /// @return witness - the encoded witness field. - function _enterWitness(uint256 rollupChainId, address rollupRecipient) internal pure returns (bytes32 witness) { - witness = keccak256(abi.encode(_ENTER_WITNESS_TYPEHASH, EnterWitness(rollupChainId, rollupRecipient))); + /// @return _witness - the hashed witness and its typestring. + function enterWitness(uint256 rollupChainId, address rollupRecipient) + public + pure + returns (Witness memory _witness) + { + _witness.witnessHash = + keccak256(abi.encode(_ENTER_WITNESS_TYPEHASH, EnterWitness(rollupChainId, rollupRecipient))); + _witness.witnessTypeString = _ENTER_WITNESS_TYPESTRING; } /// @notice Hash the hostRecipient for use as a permit2 witness. - /// @return witness - the encoded witness field. - function _exitWitness(address hostRecipient) internal pure returns (bytes32 witness) { - witness = keccak256(abi.encode(_EXIT_WITNESS_TYPEHASH, ExitWitness(hostRecipient))); + /// @return _witness - the hashed witness and its typestring. + function exitWitness(address hostRecipient) public pure returns (Witness memory _witness) { + _witness.witnessHash = keccak256(abi.encode(_EXIT_WITNESS_TYPEHASH, ExitWitness(hostRecipient))); + _witness.witnessTypeString = _EXIT_WITNESS_TYPESTRING; } /// @notice transform TokenPermissions to TransferDetails, for passing to permit2. diff --git a/test/Passage.t.sol b/test/Passage.t.sol index 1c682b0..3f61c02 100644 --- a/test/Passage.t.sol +++ b/test/Passage.t.sol @@ -21,8 +21,6 @@ contract PassageTest is Test { uint256 gas = 10_000_000; uint256 maxFeePerGas = 50; - uint256 tokenAdminKey = 123; - event Enter(uint256 indexed rollupChainId, address indexed rollupRecipient, uint256 amount); event EnterToken( From c8913cc342ab0b1c964496f500fa8969d4be2d10 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:21:33 +0100 Subject: [PATCH 06/18] minor refactor --- src/Orders.sol | 4 +- src/permit2/UsesPermit2.sol | 100 ++++++++++++++++++------------------ 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/Orders.sol b/src/Orders.sol index c982875..afea902 100644 --- a/src/Orders.sol +++ b/src/Orders.sol @@ -51,7 +51,7 @@ abstract contract OrderDestination is IOrders, OrdersPermit2 { function fillPermit2(Output[] memory outputs, OrdersPermit2.Permit2Batch calldata permit2) external { // transfer all tokens to the Output recipients via permit2 (includes check on nonce & deadline) _permitWitnessTransferFrom( - outputsWitness(outputs), _fillTransferDetails(outputs, permit2.permit.permitted), permit2 + outputWitness(outputs), _fillTransferDetails(outputs, permit2.permit.permitted), permit2 ); // emit @@ -126,7 +126,7 @@ abstract contract OrderOrigin is IOrders, OrdersPermit2 { ) external { // transfer all tokens to the tokenRecipient via permit2 (includes check on nonce & deadline) _permitWitnessTransferFrom( - outputsWitness(outputs), _initiateTransferDetails(tokenRecipient, permit2.permit.permitted), permit2 + outputWitness(outputs), _initiateTransferDetails(tokenRecipient, permit2.permit.permitted), permit2 ); // emit diff --git a/src/permit2/UsesPermit2.sol b/src/permit2/UsesPermit2.sol index 9b4d141..8cd1e7f 100644 --- a/src/permit2/UsesPermit2.sol +++ b/src/permit2/UsesPermit2.sol @@ -5,6 +5,24 @@ import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol" import {IOrders} from "../interfaces/IOrders.sol"; abstract contract UsesPermit2 { + /// @param permit - the permit2 single token transfer details. includes a `deadline` and an unordered `nonce`. + /// @param signer - the signer of the permit2 info; the owner of the tokens. + /// @param signature - the signature over the permit + witness. + struct Permit2 { + ISignatureTransfer.PermitTransferFrom permit; + address owner; + bytes signature; + } + + /// @param permit - the permit2 batch token transfer details. includes a `deadline` and an unordered `nonce`. + /// @param signer - the signer of the permit2 info; the owner of the tokens. + /// @param signature - the signature over the permit + witness. + struct Permit2Batch { + ISignatureTransfer.PermitBatchTransferFrom permit; + address owner; + bytes signature; + } + /// @notice Struct to hold the pre-hashed witness field and the witness type string. struct Witness { bytes32 witnessHash; @@ -26,18 +44,22 @@ abstract contract OrdersPermit2 is UsesPermit2 { bytes32 constant _OUTPUT_TYPEHASH = keccak256("Output(address token,uint256 amount,address recipient,uint32 chainId)"); - /// @param permit - the permit2 batch token transfer details. includes a `deadline` and an unordered `nonce`. - /// @param signer - the signer of the permit2 info; the owner of the tokens. - /// @param signature - the signature over the permit + witness. - struct Permit2Batch { - ISignatureTransfer.PermitBatchTransferFrom permit; - address owner; - bytes signature; - } - /// @notice Thrown when a signed Output does not match the corresponding TokenPermissions. error OutputMismatch(); + /// @notice Encode the Output array according to EIP-712 for use as a permit2 witness. + /// @param outputs - the Outputs to encode. + /// @return _witness - the encoded witness field. + function outputWitness(IOrders.Output[] memory outputs) public pure returns (Witness memory _witness) { + uint256 num = outputs.length; + bytes32[] memory hashes = new bytes32[](num); + for (uint256 i = 0; i < num; ++i) { + hashes[i] = keccak256(abi.encode(_OUTPUT_TYPEHASH, outputs[i])); + } + _witness.witnessHash = keccak256(abi.encodePacked(hashes)); + _witness.witnessTypeString = _OUTPUT_WITNESS_TYPESTRING; + } + /// @notice Transfer a batch of tokens using permit2. /// @param _witness - the hashed witness and its typestring. /// @param transferDetails - the TokenPermissions for the transfer, generated based on the use-case (see `_initiateTransferDetails` and `_fillTransferDetails`). @@ -57,19 +79,6 @@ abstract contract OrdersPermit2 is UsesPermit2 { ); } - /// @notice Encode the Output array according to EIP-712 for use as a permit2 witness. - /// @param outputs - the Outputs to encode. - /// @return _witness - the encoded witness field. - function outputsWitness(IOrders.Output[] memory outputs) public pure returns (Witness memory _witness) { - uint256 num = outputs.length; - bytes32[] memory hashes = new bytes32[](num); - for (uint256 i = 0; i < num; ++i) { - hashes[i] = keccak256(abi.encode(_OUTPUT_TYPEHASH, outputs[i])); - } - _witness.witnessHash = keccak256(abi.encodePacked(hashes)); - _witness.witnessTypeString = _OUTPUT_WITNESS_TYPESTRING; - } - /// @notice transform Output and TokenPermissions structs to TransferDetails structs, for passing to permit2. /// @dev always transfers the full permitted amount. /// @param outputs - the Outputs to transform. @@ -142,29 +151,6 @@ abstract contract PassagePermit2 is UsesPermit2 { address hostRecipient; } - /// @param permit - the permit2 single token transfer details. includes a `deadline` and an unordered `nonce`. - /// @param signer - the signer of the permit2 info; the owner of the tokens. - /// @param signature - the signature over the permit + witness. - struct Permit2 { - ISignatureTransfer.PermitTransferFrom permit; - address owner; - bytes signature; - } - - /// @notice Transfer tokens using permit2. - /// @param _witness - the hashed witness and its typestring. - /// @param permit2 - the Permit2 information. - function _permitWitnessTransferFrom(Witness memory _witness, Permit2 calldata permit2) internal { - ISignatureTransfer(permit2Contract).permitWitnessTransferFrom( - permit2.permit, - _passageTransferDetails(permit2.permit.permitted), - permit2.owner, - _witness.witnessHash, - _witness.witnessTypeString, - permit2.signature - ); - } - /// @notice Encode & hash the rollupChainId and rollupRecipient for use as a permit2 witness. /// @return _witness - the hashed witness and its typestring. function enterWitness(uint256 rollupChainId, address rollupRecipient) @@ -184,16 +170,30 @@ abstract contract PassagePermit2 is UsesPermit2 { _witness.witnessTypeString = _EXIT_WITNESS_TYPESTRING; } - /// @notice transform TokenPermissions to TransferDetails, for passing to permit2. - /// @dev always transfers the full permitted amount to address(this). - /// @param permitted - the TokenPermissions to transform. + /// @notice Transfer tokens using permit2. + /// @param _witness - the hashed witness and its typestring. + /// @param permit2 - the Permit2 information. + function _permitWitnessTransferFrom(Witness memory _witness, Permit2 calldata permit2) internal { + ISignatureTransfer(permit2Contract).permitWitnessTransferFrom( + permit2.permit, + _selfTransferDetails(permit2.permit.permitted.amount), + permit2.owner, + _witness.witnessHash, + _witness.witnessTypeString, + permit2.signature + ); + } + + /// @notice Construct TransferDetails transferring a balance to this contract, for passing to permit2. + /// @dev always transfers the full amount to address(this). + /// @param amount - the amount to transfer to this contract. /// @return transferDetails - the SignatureTransferDetails generated. - function _passageTransferDetails(ISignatureTransfer.TokenPermissions calldata permitted) + function _selfTransferDetails(uint256 amount) internal view returns (ISignatureTransfer.SignatureTransferDetails memory transferDetails) { transferDetails.to = address(this); - transferDetails.requestedAmount = permitted.amount; + transferDetails.requestedAmount = amount; } } From 46e95458463f53dc9483cf52b5b1ed59ba83056c Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:22:18 +0100 Subject: [PATCH 07/18] snapshot --- .gas-snapshot | 56 +++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 0bc8776..d01040e 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,38 +1,38 @@ -OrdersTest:test_fill_ERC20() (gas: 70515) -OrdersTest:test_fill_ETH() (gas: 68476) -OrdersTest:test_fill_both() (gas: 166751) -OrdersTest:test_fill_multiETH() (gas: 132097) -OrdersTest:test_fill_underflowETH() (gas: 115381) -OrdersTest:test_initiate_ERC20() (gas: 81614) -OrdersTest:test_initiate_ETH() (gas: 45128) -OrdersTest:test_initiate_both() (gas: 118889) -OrdersTest:test_initiate_multiERC20() (gas: 722620) -OrdersTest:test_initiate_multiETH() (gas: 75516) -OrdersTest:test_orderExpired() (gas: 28084) -OrdersTest:test_sweepERC20() (gas: 60469) -OrdersTest:test_sweepETH() (gas: 82142) -OrdersTest:test_underflowETH() (gas: 63668) -PassageTest:test_configureEnter() (gas: 125672) -PassageTest:test_disallowedEnter() (gas: 56597) -PassageTest:test_enter() (gas: 25507) +OrdersTest:test_fill_ERC20() (gas: 70537) +OrdersTest:test_fill_ETH() (gas: 68498) +OrdersTest:test_fill_both() (gas: 166773) +OrdersTest:test_fill_multiETH() (gas: 132119) +OrdersTest:test_fill_underflowETH() (gas: 115403) +OrdersTest:test_initiate_ERC20() (gas: 81636) +OrdersTest:test_initiate_ETH() (gas: 45150) +OrdersTest:test_initiate_both() (gas: 118911) +OrdersTest:test_initiate_multiERC20() (gas: 722642) +OrdersTest:test_initiate_multiETH() (gas: 75538) +OrdersTest:test_orderExpired() (gas: 28106) +OrdersTest:test_sweepERC20() (gas: 60491) +OrdersTest:test_sweepETH() (gas: 82186) +OrdersTest:test_underflowETH() (gas: 63690) +PassageTest:test_configureEnter() (gas: 125771) +PassageTest:test_disallowedEnter() (gas: 56619) +PassageTest:test_enter() (gas: 25519) PassageTest:test_enterToken() (gas: 64397) -PassageTest:test_enterToken_defaultChain() (gas: 62935) -PassageTest:test_enter_defaultChain() (gas: 24033) -PassageTest:test_fallback() (gas: 21489) +PassageTest:test_enterToken_defaultChain() (gas: 62979) +PassageTest:test_enter_defaultChain() (gas: 24055) +PassageTest:test_fallback() (gas: 21533) PassageTest:test_onlyTokenAdmin() (gas: 16881) -PassageTest:test_receive() (gas: 21361) -PassageTest:test_setUp() (gas: 16923) +PassageTest:test_receive() (gas: 21383) +PassageTest:test_setUp() (gas: 17011) PassageTest:test_withdraw() (gas: 59188) -RollupPassageTest:test_exit() (gas: 22369) -RollupPassageTest:test_exitToken() (gas: 50214) -RollupPassageTest:test_fallback() (gas: 19905) +RollupPassageTest:test_exit() (gas: 22403) +RollupPassageTest:test_exitToken() (gas: 50232) +RollupPassageTest:test_fallback() (gas: 19949) RollupPassageTest:test_receive() (gas: 19844) TransactTest:test_configureGas() (gas: 22828) -TransactTest:test_enterTransact() (gas: 103961) +TransactTest:test_enterTransact() (gas: 103973) TransactTest:test_onlyGasAdmin() (gas: 8810) TransactTest:test_setUp() (gas: 17494) -TransactTest:test_transact() (gas: 101431) -TransactTest:test_transact_defaultChain() (gas: 100544) +TransactTest:test_transact() (gas: 101443) +TransactTest:test_transact_defaultChain() (gas: 100556) TransactTest:test_transact_globalGasLimit() (gas: 105063) TransactTest:test_transact_perTransactGasLimit() (gas: 32774) ZenithTest:test_addSequencer() (gas: 88121) From 965266d8fe6aec1ee8c4d110666d2a3e4a74ccdd Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:42:40 +0100 Subject: [PATCH 08/18] function visibility & ordering --- src/Orders.sol | 56 ++++++++++++++++++++++++------------------------ src/Passage.sol | 20 ++++++++--------- src/Transact.sol | 2 +- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/Orders.sol b/src/Orders.sol index afea902..31862c7 100644 --- a/src/Orders.sol +++ b/src/Orders.sol @@ -24,20 +24,6 @@ abstract contract OrderDestination is IOrders, OrdersPermit2 { emit Filled(outputs); } - /// @notice Transfer the Order Outputs to their recipients. - function _transferOutputs(Output[] memory outputs) internal { - uint256 value = msg.value; - for (uint256 i; i < outputs.length; i++) { - if (outputs[i].token == address(0)) { - // this line should underflow if there's an attempt to spend more ETH than is attached to the transaction - value -= outputs[i].amount; - payable(outputs[i].recipient).transfer(outputs[i].amount); - } else { - IERC20(outputs[i].token).transferFrom(msg.sender, outputs[i].recipient, outputs[i].amount); - } - } - } - /// @notice Fill any number of Order(s), by transferring their Output(s) via permit2 signed batch transfer. /// @dev Can only provide ERC20 tokens as Outputs. /// @dev Filler may aggregate multiple Outputs with the same (`chainId`, `recipient`, `token`) into a single Output with the summed `amount`. @@ -57,6 +43,20 @@ abstract contract OrderDestination is IOrders, OrdersPermit2 { // emit emit Filled(outputs); } + + /// @notice Transfer the Order Outputs to their recipients. + function _transferOutputs(Output[] memory outputs) internal { + uint256 value = msg.value; + for (uint256 i; i < outputs.length; i++) { + if (outputs[i].token == address(0)) { + // this line should underflow if there's an attempt to spend more ETH than is attached to the transaction + value -= outputs[i].amount; + payable(outputs[i].recipient).transfer(outputs[i].amount); + } else { + IERC20(outputs[i].token).transferFrom(msg.sender, outputs[i].recipient, outputs[i].amount); + } + } + } } /// @notice Contract capable of registering initiation of intent-based Orders. @@ -98,19 +98,6 @@ abstract contract OrderOrigin is IOrders, OrdersPermit2 { emit Order(deadline, inputs, outputs); } - /// @notice Transfer the Order inputs to this contract, where they can be collected by the Order filler via `sweep`. - function _transferInputs(Input[] memory inputs) internal { - uint256 value = msg.value; - for (uint256 i; i < inputs.length; i++) { - if (inputs[i].token == address(0)) { - // this line should underflow if there's an attempt to spend more ETH than is attached to the transaction - value -= inputs[i].amount; - } else { - IERC20(inputs[i].token).transferFrom(msg.sender, address(this), inputs[i].amount); - } - } - } - /// @notice Initiate an Order, transferring Input tokens to the Filler via permit2 signed batch transfer. /// @dev Can only provide ERC20 tokens as Inputs. /// @dev the permit2 signer is the swapper providing the Input tokens in exchange for the Outputs. @@ -141,7 +128,7 @@ abstract contract OrderOrigin is IOrders, OrdersPermit2 { /// @param token - The token to transfer. /// @custom:emits Sweep /// @custom:reverts OnlyBuilder if called by non-block builder - function sweep(address recipient, address token) public { + function sweep(address recipient, address token) external { // send ETH or tokens uint256 balance; if (token == address(0)) { @@ -153,6 +140,19 @@ abstract contract OrderOrigin is IOrders, OrdersPermit2 { } emit Sweep(recipient, token, balance); } + + /// @notice Transfer the Order inputs to this contract, where they can be collected by the Order filler via `sweep`. + function _transferInputs(Input[] memory inputs) internal { + uint256 value = msg.value; + for (uint256 i; i < inputs.length; i++) { + if (inputs[i].token == address(0)) { + // this line should underflow if there's an attempt to spend more ETH than is attached to the transaction + value -= inputs[i].amount; + } else { + IERC20(inputs[i].token).transferFrom(msg.sender, address(this), inputs[i].amount); + } + } + } } contract HostOrders is OrderDestination { diff --git a/src/Passage.sol b/src/Passage.sol index fc1d1a3..a1faded 100644 --- a/src/Passage.sol +++ b/src/Passage.sol @@ -106,7 +106,7 @@ contract Passage is PassagePermit2 { /// @param rollupRecipient - The recipient of tokens on the rollup. /// @param permit2 - The Permit2 information, including token & amount. function enterTokenPermit2(uint256 rollupChainId, address rollupRecipient, PassagePermit2.Permit2 calldata permit2) - public + external { // transfer tokens to this contract via permit2 _permitWitnessTransferFrom(enterWitness(rollupChainId, rollupRecipient), permit2); @@ -114,13 +114,6 @@ contract Passage is PassagePermit2 { _enterToken(rollupChainId, rollupRecipient, permit2.permit.permitted.token, permit2.permit.permitted.amount); } - /// @notice Shared functionality for tokens entering rollup. - function _enterToken(uint256 rollupChainId, address rollupRecipient, address token, uint256 amount) internal { - if (amount == 0) return; - if (!canEnter[token]) revert DisallowedEnter(token); - emit EnterToken(rollupChainId, rollupRecipient, token, amount); - } - /// @notice Alow/Disallow a given ERC20 token to enter the rollup. function configureEnter(address token, bool _canEnter) external { if (msg.sender != tokenAdmin) revert OnlyTokenAdmin(); @@ -139,6 +132,13 @@ contract Passage is PassagePermit2 { emit Withdrawal(token, recipient, amount); } + /// @notice Shared functionality for tokens entering rollup. + function _enterToken(uint256 rollupChainId, address rollupRecipient, address token, uint256 amount) internal { + if (amount == 0) return; + if (!canEnter[token]) revert DisallowedEnter(token); + emit EnterToken(rollupChainId, rollupRecipient, token, amount); + } + /// @notice Helper to configure ERC20 enters on deploy & via admin function function _configureEnter(address token, bool _canEnter) internal { canEnter[token] = _canEnter; @@ -184,7 +184,7 @@ contract RollupPassage is PassagePermit2 { /// @param token - The rollup address of the token exiting the rollup. /// @param amount - The amount of tokens exiting the rollup. /// @custom:emits ExitToken - function exitToken(address hostRecipient, address token, uint256 amount) public { + function exitToken(address hostRecipient, address token, uint256 amount) external { // transfer tokens to this contract IERC20(token).transferFrom(msg.sender, address(this), amount); // burn and emit @@ -195,7 +195,7 @@ contract RollupPassage is PassagePermit2 { /// @param hostRecipient - The *requested* recipient of tokens on the host chain. /// @param permit2 - The Permit2 information, including token & amount. /// @custom:emits ExitToken - function exitTokenPermit2(address hostRecipient, PassagePermit2.Permit2 calldata permit2) public { + function exitTokenPermit2(address hostRecipient, PassagePermit2.Permit2 calldata permit2) external { // transfer tokens to this contract _permitWitnessTransferFrom(exitWitness(hostRecipient), permit2); // burn and emit diff --git a/src/Transact.sol b/src/Transact.sol index 3c738a3..0faedd3 100644 --- a/src/Transact.sol +++ b/src/Transact.sol @@ -78,7 +78,7 @@ contract Transactor { uint256 value, uint256 gas, uint256 maxFeePerGas - ) public payable { + ) external payable { enterTransact(rollupChainId, msg.sender, to, data, value, gas, maxFeePerGas); } From 85788f8a106d34d72341b2e08eaf18b04d026afa Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:20:52 +0100 Subject: [PATCH 09/18] test: permit2 flows --- test/Helpers.t.sol | 126 ++++++++++++++++- test/Permit2.t.sol | 342 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 test/Permit2.t.sol diff --git a/test/Helpers.t.sol b/test/Helpers.t.sol index c040458..1453adf 100644 --- a/test/Helpers.t.sol +++ b/test/Helpers.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {Test, console2} from "forge-std/Test.sol"; import {Zenith} from "../src/Zenith.sol"; +import {UsesPermit2} from "../src/permit2/UsesPermit2.sol"; + +import {Test, console2} from "forge-std/Test.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; contract TestERC20 is ERC20Burnable { constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} @@ -14,6 +17,127 @@ contract TestERC20 is ERC20Burnable { } } +contract Permit2Stub { + /// @notice stubbed `permitWitnessTransferFrom` - does not check signature, nonce, or deadline + function permitWitnessTransferFrom( + ISignatureTransfer.PermitTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails calldata transferDetails, + address owner, + bytes32, /*witness*/ + string calldata, /*witnessTypeString*/ + bytes calldata /*signature*/ + ) external { + ERC20(permit.permitted.token).transferFrom(owner, transferDetails.to, transferDetails.requestedAmount); + } +} + +contract BatchPermit2Stub { + function permitWitnessTransferFrom( + ISignatureTransfer.PermitBatchTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes32, /*witness*/ + string calldata, /*witnessTypeString*/ + bytes calldata /*signature*/ + ) external { + for (uint256 i = 0; i < transferDetails.length; i++) { + ERC20(permit.permitted[i].token).transferFrom( + owner, transferDetails[i].to, transferDetails[i].requestedAmount + ); + } + } +} + +contract Permit2Helpers is Test { + string public constant _PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB = + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,"; + + bytes32 public constant _TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); + + string public constant _PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB = + "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,"; + + /// @notice given a Permit and a Witness, produce a signature from the `owner` + function signPermit( + uint256 signingKey, + address spender, + ISignatureTransfer.PermitTransferFrom memory permit, + UsesPermit2.Witness memory _witness + ) internal pure returns (bytes memory signature) { + bytes32 permit2Hash = hashWithWitness(spender, permit, _witness.witnessHash, _witness.witnessTypeString); + uint8 v; + bytes32 r; + bytes32 s; + (v, r, s) = vm.sign(signingKey, permit2Hash); + signature = abi.encodePacked(r, s, v); + } + + // this function is private on permit2 contracts but need to port it here for test functionality + function hashWithWitness( + address spender, + ISignatureTransfer.PermitTransferFrom memory _permit, + bytes32 witness, + string memory witnessTypeString + ) internal pure returns (bytes32) { + bytes32 typeHash = keccak256(abi.encodePacked(_PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, witnessTypeString)); + + bytes32 tokenPermissionsHash = _hashTokenPermissions(_permit.permitted); + return keccak256(abi.encode(typeHash, tokenPermissionsHash, spender, _permit.nonce, _permit.deadline, witness)); + } + + /// @notice given a Permit and a Witness, produce a signature from the `owner` + function signPermit( + uint256 signingKey, + address spender, + ISignatureTransfer.PermitBatchTransferFrom memory permit, + UsesPermit2.Witness memory _witness + ) internal pure returns (bytes memory signature) { + bytes32 permit2Hash = hashWithWitness(spender, permit, _witness.witnessHash, _witness.witnessTypeString); + uint8 v; + bytes32 r; + bytes32 s; + (v, r, s) = vm.sign(signingKey, permit2Hash); + signature = abi.encodePacked(r, s, v); + } + + function hashWithWitness( + address spender, + ISignatureTransfer.PermitBatchTransferFrom memory permit, + bytes32 witness, + string memory witnessTypeString + ) internal pure returns (bytes32) { + bytes32 typeHash = + keccak256(abi.encodePacked(_PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB, witnessTypeString)); + + uint256 numPermitted = permit.permitted.length; + bytes32[] memory tokenPermissionHashes = new bytes32[](numPermitted); + + for (uint256 i = 0; i < numPermitted; ++i) { + tokenPermissionHashes[i] = _hashTokenPermissions(permit.permitted[i]); + } + + return keccak256( + abi.encode( + typeHash, + keccak256(abi.encodePacked(tokenPermissionHashes)), + spender, + permit.nonce, + permit.deadline, + witness + ) + ); + } + + // this function is private on permit2 contracts but need to port it here for test functionality + function _hashTokenPermissions(ISignatureTransfer.TokenPermissions memory _permitted) + private + pure + returns (bytes32) + { + return keccak256(abi.encode(_TOKEN_PERMISSIONS_TYPEHASH, _permitted)); + } +} + contract HelpersTest is Test { Zenith public target; diff --git a/test/Permit2.t.sol b/test/Permit2.t.sol new file mode 100644 index 0000000..72bb15b --- /dev/null +++ b/test/Permit2.t.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +// test contracts +import {Passage, RollupPassage} from "../src/Passage.sol"; +import {PassagePermit2, UsesPermit2} from "../src/permit2/UsesPermit2.sol"; +import {RollupOrders} from "../src/Orders.sol"; +import {IOrders} from "../src/interfaces/IOrders.sol"; + +// Permit2 deps +// import {Permit2} from "permit2/src/Permit2.sol"; +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; +import {PermitHash} from "permit2/src/libraries/PermitHash.sol"; + +// other test utils +import {Permit2Helpers, Permit2Stub, BatchPermit2Stub, TestERC20} from "./Helpers.t.sol"; +import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {Test, console2} from "forge-std/Test.sol"; + +// TODO: test that witness encoding is valid +// TODO: fix gas metering in the tests + +contract SharedPermit2Test is Permit2Helpers { + Permit2Stub permit2Contract; + BatchPermit2Stub permit2BatchContract; + + /// @notice the address signing the Permit messages and its pk + uint256 ownerKey = 123; + address owner = vm.addr(ownerKey); + + // permit consts + UsesPermit2.Witness witness; + // single permit + UsesPermit2.Permit2 permit2; + ISignatureTransfer.SignatureTransferDetails transferDetails; + // batch permit + UsesPermit2.Permit2Batch permit2Batch; + ISignatureTransfer.SignatureTransferDetails[] transferDetailsBatch; + + function _setUpPermit2(address token, uint256 amount) internal { + vm.label(owner, "owner"); + + // deploy permit2 + permit2Contract = new Permit2Stub(); + vm.label(address(permit2Contract), "permit2"); + + // deploy batch permit2 + permit2BatchContract = new BatchPermit2Stub(); + vm.label(address(permit2BatchContract), "permit2Batch"); + + // approve permit2 & batch permit2 + vm.prank(owner); + TestERC20(token).approve(address(permit2Contract), amount * 10000); + vm.prank(owner); + TestERC20(token).approve(address(permit2BatchContract), amount * 10000); + + // create a single permit with generic details + permit2.permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: token, amount: amount}), + nonce: 0, + deadline: block.timestamp + }); + permit2.owner = owner; + + // create a batch permit with generic details + permit2Batch.permit.permitted.push(ISignatureTransfer.TokenPermissions({token: token, amount: amount})); + permit2Batch.permit.nonce = 0; + permit2Batch.permit.deadline = block.timestamp; + permit2Batch.owner = owner; + } +} + +contract PassagePermit2Test is SharedPermit2Test { + Passage public target; + + // token consts + address token; + uint256 amount = 200; + uint256 chainId = 3; + address recipient = address(0x123); + + event EnterToken( + uint256 indexed rollupChainId, address indexed rollupRecipient, address indexed token, uint256 amount + ); + + function setUp() public { + // deploy token + token = address(new TestERC20("hi", "HI")); + TestERC20(token).mint(owner, amount * 10000); + + // configure token for passage + address[] memory initialEnterTokens = new address[](2); + initialEnterTokens[0] = token; + + // setup permit2 contract & permit details + _setUpPermit2(token, amount); + + // deploy Passage + target = new Passage(block.chainid + 1, address(this), initialEnterTokens, address(permit2Contract)); + vm.label(address(target), "passage"); + + // construct Enter witness + witness = target.enterWitness(chainId, recipient); + + // sign permit + witness + permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); + + // construct transfer details + transferDetails = ISignatureTransfer.SignatureTransferDetails({to: address(target), requestedAmount: amount}); + } + + function test_enterTokenPermit2() public { + vm.expectEmit(); + emit EnterToken(chainId, recipient, token, amount); + vm.expectCall( + address(permit2Contract), + abi.encodeWithSelector( + Permit2Stub.permitWitnessTransferFrom.selector, + permit2.permit, + transferDetails, + owner, + witness.witnessHash, + witness.witnessTypeString, + permit2.signature + ) + ); + vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, address(target), amount)); + target.enterTokenPermit2(chainId, recipient, permit2); + } + + function test_disallowedEnterPermit2() public { + // deploy new token & approve permit2 + address newToken = address(new TestERC20("bye", "BYE")); + TestERC20(newToken).mint(owner, amount * 10000); + vm.prank(owner); + TestERC20(newToken).approve(address(permit2Contract), amount * 10000); + + // edit permit token to new token + permit2.permit.permitted.token = newToken; + + // re-sign permit + witness + permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); + + // expect revert DisallowedEnter + vm.expectRevert(abi.encodeWithSelector(Passage.DisallowedEnter.selector, newToken)); + target.enterTokenPermit2(chainId, recipient, permit2); + } +} + +contract RollupPassagePermit2Test is SharedPermit2Test { + RollupPassage public target; + + // token consts + address token; + uint256 amount = 200; + uint256 chainId = 3; + address recipient = address(0x123); + + event ExitToken(address indexed hostRecipient, address indexed token, uint256 amount); + + function setUp() public { + // deploy token & approve permit2 + token = address(new TestERC20("hi", "HI")); + TestERC20(token).mint(owner, amount * 10000); + + // setup permit2 contract & permit details + _setUpPermit2(token, amount); + + // deploy Passage + target = new RollupPassage(address(permit2Contract)); + vm.label(address(target), "passage"); + + // construct Exit witness + witness = target.exitWitness(recipient); + + // sign permit + witness + permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); + + // construct transfer details + transferDetails = ISignatureTransfer.SignatureTransferDetails({to: address(target), requestedAmount: amount}); + } + + function test_exitTokenPermit2() public { + vm.expectEmit(); + emit ExitToken(recipient, token, amount); + vm.expectCall( + address(permit2Contract), + abi.encodeWithSelector( + Permit2Stub.permitWitnessTransferFrom.selector, + permit2.permit, + transferDetails, + owner, + witness.witnessHash, + witness.witnessTypeString, + permit2.signature + ) + ); + vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, address(target), amount)); + vm.expectCall(token, abi.encodeWithSelector(ERC20Burnable.burn.selector, amount)); + target.exitTokenPermit2(recipient, permit2); + } +} + +contract OrderOriginPermit2Test is SharedPermit2Test { + RollupOrders public target; + + IOrders.Input[] public inputs; + IOrders.Output[] public outputs; + + mapping(address => bool) isToken; + + address token; + uint32 chainId = 3; + address recipient = address(0x123); + uint256 amount = 200; + uint256 deadline = block.timestamp; + + address tokenRecipient = address(0xdeadbeef); + + event Order(uint256 deadline, IOrders.Input[] inputs, IOrders.Output[] outputs); + + event Filled(IOrders.Output[] outputs); + + function setUp() public { + // deploy token + token = address(new TestERC20("hi", "HI")); + TestERC20(token).mint(owner, amount * 10000); + isToken[token] = true; + + // setup permit2 contract & permit details + _setUpPermit2(token, amount); + + // deploy Orders contract + target = new RollupOrders(address(permit2BatchContract)); + vm.label(address(target), "orders"); + + // setup Order Inputs/Outputs + IOrders.Input memory input = IOrders.Input(token, amount); + inputs.push(input); + + IOrders.Output memory output = IOrders.Output(token, amount, recipient, chainId); + outputs.push(output); + + // construct Orders witness + witness = target.outputWitness(outputs); + + // sign permit + witness + permit2.signature = signPermit(ownerKey, address(target), permit2Batch.permit, witness); + + // construct transfer details + transferDetailsBatch.push( + ISignatureTransfer.SignatureTransferDetails({to: tokenRecipient, requestedAmount: amount}) + ); + } + + function test_initiatePermit2() public { + // expect Order event is initiated, ERC20 is transferred + vm.expectEmit(); + emit Order(deadline, inputs, outputs); + vm.expectCall( + address(permit2BatchContract), + abi.encodeWithSelector( + BatchPermit2Stub.permitWitnessTransferFrom.selector, + permit2Batch.permit, + transferDetailsBatch, + owner, + witness.witnessHash, + witness.witnessTypeString, + permit2Batch.signature + ) + ); + vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, tokenRecipient, amount)); + target.initiatePermit2(tokenRecipient, outputs, permit2Batch); + } + + // input multiple ERC20s + function test_initiatePermit2_multi() public { + // setup second token + address token2 = address(new TestERC20("bye", "BYE")); + TestERC20(token2).mint(owner, amount * 10000); + vm.prank(owner); + TestERC20(token2).approve(address(permit2BatchContract), amount * 10000); + + // add second token input + inputs.push(IOrders.Input(token2, amount * 2)); + + // add TokenPermissions + permit2Batch.permit.permitted.push(ISignatureTransfer.TokenPermissions({token: token2, amount: amount * 2})); + + // add TransferDetails + transferDetailsBatch.push( + ISignatureTransfer.SignatureTransferDetails({to: tokenRecipient, requestedAmount: amount * 2}) + ); + + // re-sign new permit + permit2.signature = signPermit(ownerKey, address(target), permit2Batch.permit, witness); + + // expect Order event is emitted, ERC20 is transferred + vm.expectEmit(); + emit Order(deadline, inputs, outputs); + vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, tokenRecipient, amount)); + vm.expectCall(token2, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, tokenRecipient, amount * 2)); + target.initiatePermit2(tokenRecipient, outputs, permit2Batch); + } + + function test_fillPermit2() public { + vm.expectEmit(); + emit Filled(outputs); + vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, recipient, amount)); + target.fillPermit2(outputs, permit2Batch); + } + + function test_fillPermit2_multi() public { + // setup second token + address token2 = address(new TestERC20("bye", "BYE")); + TestERC20(token2).mint(owner, amount * 10000); + vm.prank(owner); + TestERC20(token2).approve(address(permit2BatchContract), amount * 10000); + + // add second token output + outputs.push(IOrders.Output(token2, amount * 2, recipient, chainId)); + + // add TokenPermissions + permit2Batch.permit.permitted.push(ISignatureTransfer.TokenPermissions({token: token2, amount: amount * 2})); + + // add TransferDetails + transferDetailsBatch.push( + ISignatureTransfer.SignatureTransferDetails({to: recipient, requestedAmount: amount * 2}) + ); + + // re-sign new permit + permit2.signature = signPermit(ownerKey, address(target), permit2Batch.permit, witness); + + // expect Filled event is emitted, ERC20 is transferred + vm.expectEmit(); + emit Filled(outputs); + vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, recipient, amount)); + vm.expectCall(token2, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, recipient, amount * 2)); + target.fillPermit2(outputs, permit2Batch); + } +} From 7d07528e8ed2c3d22b2abebc1ecbd8079ae093c2 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:22:54 +0100 Subject: [PATCH 10/18] snapshot --- .gas-snapshot | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gas-snapshot b/.gas-snapshot index d01040e..0c824a4 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,3 +1,7 @@ +OrderOriginPermit2Test:test_fillPermit2() (gas: 98026) +OrderOriginPermit2Test:test_fillPermit2_multi() (gas: 903748) +OrderOriginPermit2Test:test_initiatePermit2() (gas: 138528) +OrderOriginPermit2Test:test_initiatePermit2_multi() (gas: 890825) OrdersTest:test_fill_ERC20() (gas: 70537) OrdersTest:test_fill_ETH() (gas: 68498) OrdersTest:test_fill_both() (gas: 166773) @@ -12,6 +16,8 @@ OrdersTest:test_orderExpired() (gas: 28106) OrdersTest:test_sweepERC20() (gas: 60491) OrdersTest:test_sweepETH() (gas: 82186) OrdersTest:test_underflowETH() (gas: 63690) +PassagePermit2Test:test_disallowedEnterPermit2() (gas: 669476) +PassagePermit2Test:test_enterTokenPermit2() (gas: 115926) PassageTest:test_configureEnter() (gas: 125771) PassageTest:test_disallowedEnter() (gas: 56619) PassageTest:test_enter() (gas: 25519) @@ -23,6 +29,7 @@ PassageTest:test_onlyTokenAdmin() (gas: 16881) PassageTest:test_receive() (gas: 21383) PassageTest:test_setUp() (gas: 17011) PassageTest:test_withdraw() (gas: 59188) +RollupPassagePermit2Test:test_exitTokenPermit2() (gas: 99802) RollupPassageTest:test_exit() (gas: 22403) RollupPassageTest:test_exitToken() (gas: 50232) RollupPassageTest:test_fallback() (gas: 19949) From 862fef12959779c81f7a4744a0193a1f2d245637 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:25:40 +0100 Subject: [PATCH 11/18] remove TODOs --- test/Permit2.t.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/Permit2.t.sol b/test/Permit2.t.sol index 72bb15b..5f0c221 100644 --- a/test/Permit2.t.sol +++ b/test/Permit2.t.sol @@ -18,9 +18,6 @@ import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {Test, console2} from "forge-std/Test.sol"; -// TODO: test that witness encoding is valid -// TODO: fix gas metering in the tests - contract SharedPermit2Test is Permit2Helpers { Permit2Stub permit2Contract; BatchPermit2Stub permit2BatchContract; From 127cd71f995e204cef65d7d7d663d36fdca21272 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:33:05 +0100 Subject: [PATCH 12/18] split batch and single helpers --- test/Permit2.t.sol | 102 +++++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/test/Permit2.t.sol b/test/Permit2.t.sol index 5f0c221..f02b0e1 100644 --- a/test/Permit2.t.sol +++ b/test/Permit2.t.sol @@ -20,7 +20,6 @@ import {Test, console2} from "forge-std/Test.sol"; contract SharedPermit2Test is Permit2Helpers { Permit2Stub permit2Contract; - BatchPermit2Stub permit2BatchContract; /// @notice the address signing the Permit messages and its pk uint256 ownerKey = 123; @@ -31,9 +30,6 @@ contract SharedPermit2Test is Permit2Helpers { // single permit UsesPermit2.Permit2 permit2; ISignatureTransfer.SignatureTransferDetails transferDetails; - // batch permit - UsesPermit2.Permit2Batch permit2Batch; - ISignatureTransfer.SignatureTransferDetails[] transferDetailsBatch; function _setUpPermit2(address token, uint256 amount) internal { vm.label(owner, "owner"); @@ -42,16 +38,14 @@ contract SharedPermit2Test is Permit2Helpers { permit2Contract = new Permit2Stub(); vm.label(address(permit2Contract), "permit2"); - // deploy batch permit2 - permit2BatchContract = new BatchPermit2Stub(); - vm.label(address(permit2BatchContract), "permit2Batch"); - - // approve permit2 & batch permit2 + // approve permit2 vm.prank(owner); TestERC20(token).approve(address(permit2Contract), amount * 10000); - vm.prank(owner); - TestERC20(token).approve(address(permit2BatchContract), amount * 10000); + _setupSinglePermit(token, amount); + } + + function _setupSinglePermit(address token, uint256 amount) internal { // create a single permit with generic details permit2.permit = ISignatureTransfer.PermitTransferFrom({ permitted: ISignatureTransfer.TokenPermissions({token: token, amount: amount}), @@ -59,12 +53,6 @@ contract SharedPermit2Test is Permit2Helpers { deadline: block.timestamp }); permit2.owner = owner; - - // create a batch permit with generic details - permit2Batch.permit.permitted.push(ISignatureTransfer.TokenPermissions({token: token, amount: amount})); - permit2Batch.permit.nonce = 0; - permit2Batch.permit.deadline = block.timestamp; - permit2Batch.owner = owner; } } @@ -199,7 +187,43 @@ contract RollupPassagePermit2Test is SharedPermit2Test { } } -contract OrderOriginPermit2Test is SharedPermit2Test { +contract Permit2BatchTest is Permit2Helpers { + BatchPermit2Stub permit2Contract; + + /// @notice the address signing the Permit messages and its pk + uint256 ownerKey = 123; + address owner = vm.addr(ownerKey); + + // permit consts + UsesPermit2.Witness witness; + // batch permit + UsesPermit2.Permit2Batch permit2; + ISignatureTransfer.SignatureTransferDetails[] transferDetails; + + function _setUpPermit2(address token, uint256 amount) internal { + vm.label(owner, "owner"); + + // deploy batch permit2 + permit2Contract = new BatchPermit2Stub(); + vm.label(address(permit2Contract), "permit2"); + + // approve batch permit2 + vm.prank(owner); + TestERC20(token).approve(address(permit2Contract), amount * 10000); + + _setupBatchPermit(token, amount); + } + + function _setupBatchPermit(address token, uint256 amount) internal { + // create a batch permit with generic details + permit2.permit.permitted.push(ISignatureTransfer.TokenPermissions({token: token, amount: amount})); + permit2.permit.nonce = 0; + permit2.permit.deadline = block.timestamp; + permit2.owner = owner; + } +} + +contract OrderOriginPermit2Test is Permit2BatchTest { RollupOrders public target; IOrders.Input[] public inputs; @@ -229,7 +253,7 @@ contract OrderOriginPermit2Test is SharedPermit2Test { _setUpPermit2(token, amount); // deploy Orders contract - target = new RollupOrders(address(permit2BatchContract)); + target = new RollupOrders(address(permit2Contract)); vm.label(address(target), "orders"); // setup Order Inputs/Outputs @@ -243,12 +267,10 @@ contract OrderOriginPermit2Test is SharedPermit2Test { witness = target.outputWitness(outputs); // sign permit + witness - permit2.signature = signPermit(ownerKey, address(target), permit2Batch.permit, witness); + permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); // construct transfer details - transferDetailsBatch.push( - ISignatureTransfer.SignatureTransferDetails({to: tokenRecipient, requestedAmount: amount}) - ); + transferDetails.push(ISignatureTransfer.SignatureTransferDetails({to: tokenRecipient, requestedAmount: amount})); } function test_initiatePermit2() public { @@ -256,19 +278,19 @@ contract OrderOriginPermit2Test is SharedPermit2Test { vm.expectEmit(); emit Order(deadline, inputs, outputs); vm.expectCall( - address(permit2BatchContract), + address(permit2Contract), abi.encodeWithSelector( BatchPermit2Stub.permitWitnessTransferFrom.selector, - permit2Batch.permit, - transferDetailsBatch, + permit2.permit, + transferDetails, owner, witness.witnessHash, witness.witnessTypeString, - permit2Batch.signature + permit2.signature ) ); vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, tokenRecipient, amount)); - target.initiatePermit2(tokenRecipient, outputs, permit2Batch); + target.initiatePermit2(tokenRecipient, outputs, permit2); } // input multiple ERC20s @@ -277,35 +299,35 @@ contract OrderOriginPermit2Test is SharedPermit2Test { address token2 = address(new TestERC20("bye", "BYE")); TestERC20(token2).mint(owner, amount * 10000); vm.prank(owner); - TestERC20(token2).approve(address(permit2BatchContract), amount * 10000); + TestERC20(token2).approve(address(permit2Contract), amount * 10000); // add second token input inputs.push(IOrders.Input(token2, amount * 2)); // add TokenPermissions - permit2Batch.permit.permitted.push(ISignatureTransfer.TokenPermissions({token: token2, amount: amount * 2})); + permit2.permit.permitted.push(ISignatureTransfer.TokenPermissions({token: token2, amount: amount * 2})); // add TransferDetails - transferDetailsBatch.push( + transferDetails.push( ISignatureTransfer.SignatureTransferDetails({to: tokenRecipient, requestedAmount: amount * 2}) ); // re-sign new permit - permit2.signature = signPermit(ownerKey, address(target), permit2Batch.permit, witness); + permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); // expect Order event is emitted, ERC20 is transferred vm.expectEmit(); emit Order(deadline, inputs, outputs); vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, tokenRecipient, amount)); vm.expectCall(token2, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, tokenRecipient, amount * 2)); - target.initiatePermit2(tokenRecipient, outputs, permit2Batch); + target.initiatePermit2(tokenRecipient, outputs, permit2); } function test_fillPermit2() public { vm.expectEmit(); emit Filled(outputs); vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, recipient, amount)); - target.fillPermit2(outputs, permit2Batch); + target.fillPermit2(outputs, permit2); } function test_fillPermit2_multi() public { @@ -313,27 +335,25 @@ contract OrderOriginPermit2Test is SharedPermit2Test { address token2 = address(new TestERC20("bye", "BYE")); TestERC20(token2).mint(owner, amount * 10000); vm.prank(owner); - TestERC20(token2).approve(address(permit2BatchContract), amount * 10000); + TestERC20(token2).approve(address(permit2Contract), amount * 10000); // add second token output outputs.push(IOrders.Output(token2, amount * 2, recipient, chainId)); // add TokenPermissions - permit2Batch.permit.permitted.push(ISignatureTransfer.TokenPermissions({token: token2, amount: amount * 2})); + permit2.permit.permitted.push(ISignatureTransfer.TokenPermissions({token: token2, amount: amount * 2})); // add TransferDetails - transferDetailsBatch.push( - ISignatureTransfer.SignatureTransferDetails({to: recipient, requestedAmount: amount * 2}) - ); + transferDetails.push(ISignatureTransfer.SignatureTransferDetails({to: recipient, requestedAmount: amount * 2})); // re-sign new permit - permit2.signature = signPermit(ownerKey, address(target), permit2Batch.permit, witness); + permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); // expect Filled event is emitted, ERC20 is transferred vm.expectEmit(); emit Filled(outputs); vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, recipient, amount)); vm.expectCall(token2, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, recipient, amount * 2)); - target.fillPermit2(outputs, permit2Batch); + target.fillPermit2(outputs, permit2); } } From d144803beb8376d4da2e83991527d1c5213a0f11 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:35:23 +0100 Subject: [PATCH 13/18] split up passage/orders tests --- test/{Permit2.t.sol => Permit2Orders.t.sol} | 175 +----------------- test/Permit2Passage.t.sol | 185 ++++++++++++++++++++ 2 files changed, 187 insertions(+), 173 deletions(-) rename test/{Permit2.t.sol => Permit2Orders.t.sol} (53%) create mode 100644 test/Permit2Passage.t.sol diff --git a/test/Permit2.t.sol b/test/Permit2Orders.t.sol similarity index 53% rename from test/Permit2.t.sol rename to test/Permit2Orders.t.sol index f02b0e1..5b6528e 100644 --- a/test/Permit2.t.sol +++ b/test/Permit2Orders.t.sol @@ -2,191 +2,20 @@ pragma solidity ^0.8.24; // test contracts -import {Passage, RollupPassage} from "../src/Passage.sol"; -import {PassagePermit2, UsesPermit2} from "../src/permit2/UsesPermit2.sol"; import {RollupOrders} from "../src/Orders.sol"; import {IOrders} from "../src/interfaces/IOrders.sol"; +import {UsesPermit2} from "../src/permit2/UsesPermit2.sol"; // Permit2 deps -// import {Permit2} from "permit2/src/Permit2.sol"; import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; import {PermitHash} from "permit2/src/libraries/PermitHash.sol"; // other test utils -import {Permit2Helpers, Permit2Stub, BatchPermit2Stub, TestERC20} from "./Helpers.t.sol"; +import {Permit2Helpers, BatchPermit2Stub, TestERC20} from "./Helpers.t.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {Test, console2} from "forge-std/Test.sol"; -contract SharedPermit2Test is Permit2Helpers { - Permit2Stub permit2Contract; - - /// @notice the address signing the Permit messages and its pk - uint256 ownerKey = 123; - address owner = vm.addr(ownerKey); - - // permit consts - UsesPermit2.Witness witness; - // single permit - UsesPermit2.Permit2 permit2; - ISignatureTransfer.SignatureTransferDetails transferDetails; - - function _setUpPermit2(address token, uint256 amount) internal { - vm.label(owner, "owner"); - - // deploy permit2 - permit2Contract = new Permit2Stub(); - vm.label(address(permit2Contract), "permit2"); - - // approve permit2 - vm.prank(owner); - TestERC20(token).approve(address(permit2Contract), amount * 10000); - - _setupSinglePermit(token, amount); - } - - function _setupSinglePermit(address token, uint256 amount) internal { - // create a single permit with generic details - permit2.permit = ISignatureTransfer.PermitTransferFrom({ - permitted: ISignatureTransfer.TokenPermissions({token: token, amount: amount}), - nonce: 0, - deadline: block.timestamp - }); - permit2.owner = owner; - } -} - -contract PassagePermit2Test is SharedPermit2Test { - Passage public target; - - // token consts - address token; - uint256 amount = 200; - uint256 chainId = 3; - address recipient = address(0x123); - - event EnterToken( - uint256 indexed rollupChainId, address indexed rollupRecipient, address indexed token, uint256 amount - ); - - function setUp() public { - // deploy token - token = address(new TestERC20("hi", "HI")); - TestERC20(token).mint(owner, amount * 10000); - - // configure token for passage - address[] memory initialEnterTokens = new address[](2); - initialEnterTokens[0] = token; - - // setup permit2 contract & permit details - _setUpPermit2(token, amount); - - // deploy Passage - target = new Passage(block.chainid + 1, address(this), initialEnterTokens, address(permit2Contract)); - vm.label(address(target), "passage"); - - // construct Enter witness - witness = target.enterWitness(chainId, recipient); - - // sign permit + witness - permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); - - // construct transfer details - transferDetails = ISignatureTransfer.SignatureTransferDetails({to: address(target), requestedAmount: amount}); - } - - function test_enterTokenPermit2() public { - vm.expectEmit(); - emit EnterToken(chainId, recipient, token, amount); - vm.expectCall( - address(permit2Contract), - abi.encodeWithSelector( - Permit2Stub.permitWitnessTransferFrom.selector, - permit2.permit, - transferDetails, - owner, - witness.witnessHash, - witness.witnessTypeString, - permit2.signature - ) - ); - vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, address(target), amount)); - target.enterTokenPermit2(chainId, recipient, permit2); - } - - function test_disallowedEnterPermit2() public { - // deploy new token & approve permit2 - address newToken = address(new TestERC20("bye", "BYE")); - TestERC20(newToken).mint(owner, amount * 10000); - vm.prank(owner); - TestERC20(newToken).approve(address(permit2Contract), amount * 10000); - - // edit permit token to new token - permit2.permit.permitted.token = newToken; - - // re-sign permit + witness - permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); - - // expect revert DisallowedEnter - vm.expectRevert(abi.encodeWithSelector(Passage.DisallowedEnter.selector, newToken)); - target.enterTokenPermit2(chainId, recipient, permit2); - } -} - -contract RollupPassagePermit2Test is SharedPermit2Test { - RollupPassage public target; - - // token consts - address token; - uint256 amount = 200; - uint256 chainId = 3; - address recipient = address(0x123); - - event ExitToken(address indexed hostRecipient, address indexed token, uint256 amount); - - function setUp() public { - // deploy token & approve permit2 - token = address(new TestERC20("hi", "HI")); - TestERC20(token).mint(owner, amount * 10000); - - // setup permit2 contract & permit details - _setUpPermit2(token, amount); - - // deploy Passage - target = new RollupPassage(address(permit2Contract)); - vm.label(address(target), "passage"); - - // construct Exit witness - witness = target.exitWitness(recipient); - - // sign permit + witness - permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); - - // construct transfer details - transferDetails = ISignatureTransfer.SignatureTransferDetails({to: address(target), requestedAmount: amount}); - } - - function test_exitTokenPermit2() public { - vm.expectEmit(); - emit ExitToken(recipient, token, amount); - vm.expectCall( - address(permit2Contract), - abi.encodeWithSelector( - Permit2Stub.permitWitnessTransferFrom.selector, - permit2.permit, - transferDetails, - owner, - witness.witnessHash, - witness.witnessTypeString, - permit2.signature - ) - ); - vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, address(target), amount)); - vm.expectCall(token, abi.encodeWithSelector(ERC20Burnable.burn.selector, amount)); - target.exitTokenPermit2(recipient, permit2); - } -} - contract Permit2BatchTest is Permit2Helpers { BatchPermit2Stub permit2Contract; diff --git a/test/Permit2Passage.t.sol b/test/Permit2Passage.t.sol new file mode 100644 index 0000000..854b82c --- /dev/null +++ b/test/Permit2Passage.t.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +// test contracts +import {Passage, RollupPassage} from "../src/Passage.sol"; +import {UsesPermit2} from "../src/permit2/UsesPermit2.sol"; + +// Permit2 deps +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; +import {PermitHash} from "permit2/src/libraries/PermitHash.sol"; + +// other test utils +import {Permit2Helpers, Permit2Stub, BatchPermit2Stub, TestERC20} from "./Helpers.t.sol"; +import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import {Test, console2} from "forge-std/Test.sol"; + +contract SharedPermit2Test is Permit2Helpers { + Permit2Stub permit2Contract; + + /// @notice the address signing the Permit messages and its pk + uint256 ownerKey = 123; + address owner = vm.addr(ownerKey); + + // permit consts + UsesPermit2.Witness witness; + // single permit + UsesPermit2.Permit2 permit2; + ISignatureTransfer.SignatureTransferDetails transferDetails; + + function _setUpPermit2(address token, uint256 amount) internal { + vm.label(owner, "owner"); + + // deploy permit2 + permit2Contract = new Permit2Stub(); + vm.label(address(permit2Contract), "permit2"); + + // approve permit2 + vm.prank(owner); + TestERC20(token).approve(address(permit2Contract), amount * 10000); + + _setupSinglePermit(token, amount); + } + + function _setupSinglePermit(address token, uint256 amount) internal { + // create a single permit with generic details + permit2.permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: token, amount: amount}), + nonce: 0, + deadline: block.timestamp + }); + permit2.owner = owner; + } +} + +contract PassagePermit2Test is SharedPermit2Test { + Passage public target; + + // token consts + address token; + uint256 amount = 200; + uint256 chainId = 3; + address recipient = address(0x123); + + event EnterToken( + uint256 indexed rollupChainId, address indexed rollupRecipient, address indexed token, uint256 amount + ); + + function setUp() public { + // deploy token + token = address(new TestERC20("hi", "HI")); + TestERC20(token).mint(owner, amount * 10000); + + // configure token for passage + address[] memory initialEnterTokens = new address[](2); + initialEnterTokens[0] = token; + + // setup permit2 contract & permit details + _setUpPermit2(token, amount); + + // deploy Passage + target = new Passage(block.chainid + 1, address(this), initialEnterTokens, address(permit2Contract)); + vm.label(address(target), "passage"); + + // construct Enter witness + witness = target.enterWitness(chainId, recipient); + + // sign permit + witness + permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); + + // construct transfer details + transferDetails = ISignatureTransfer.SignatureTransferDetails({to: address(target), requestedAmount: amount}); + } + + function test_enterTokenPermit2() public { + vm.expectEmit(); + emit EnterToken(chainId, recipient, token, amount); + vm.expectCall( + address(permit2Contract), + abi.encodeWithSelector( + Permit2Stub.permitWitnessTransferFrom.selector, + permit2.permit, + transferDetails, + owner, + witness.witnessHash, + witness.witnessTypeString, + permit2.signature + ) + ); + vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, address(target), amount)); + target.enterTokenPermit2(chainId, recipient, permit2); + } + + function test_disallowedEnterPermit2() public { + // deploy new token & approve permit2 + address newToken = address(new TestERC20("bye", "BYE")); + TestERC20(newToken).mint(owner, amount * 10000); + vm.prank(owner); + TestERC20(newToken).approve(address(permit2Contract), amount * 10000); + + // edit permit token to new token + permit2.permit.permitted.token = newToken; + + // re-sign permit + witness + permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); + + // expect revert DisallowedEnter + vm.expectRevert(abi.encodeWithSelector(Passage.DisallowedEnter.selector, newToken)); + target.enterTokenPermit2(chainId, recipient, permit2); + } +} + +contract RollupPassagePermit2Test is SharedPermit2Test { + RollupPassage public target; + + // token consts + address token; + uint256 amount = 200; + uint256 chainId = 3; + address recipient = address(0x123); + + event ExitToken(address indexed hostRecipient, address indexed token, uint256 amount); + + function setUp() public { + // deploy token & approve permit2 + token = address(new TestERC20("hi", "HI")); + TestERC20(token).mint(owner, amount * 10000); + + // setup permit2 contract & permit details + _setUpPermit2(token, amount); + + // deploy Passage + target = new RollupPassage(address(permit2Contract)); + vm.label(address(target), "passage"); + + // construct Exit witness + witness = target.exitWitness(recipient); + + // sign permit + witness + permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); + + // construct transfer details + transferDetails = ISignatureTransfer.SignatureTransferDetails({to: address(target), requestedAmount: amount}); + } + + function test_exitTokenPermit2() public { + vm.expectEmit(); + emit ExitToken(recipient, token, amount); + vm.expectCall( + address(permit2Contract), + abi.encodeWithSelector( + Permit2Stub.permitWitnessTransferFrom.selector, + permit2.permit, + transferDetails, + owner, + witness.witnessHash, + witness.witnessTypeString, + permit2.signature + ) + ); + vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, address(target), amount)); + vm.expectCall(token, abi.encodeWithSelector(ERC20Burnable.burn.selector, amount)); + target.exitTokenPermit2(recipient, permit2); + } +} From d7e21bb49305df701be82c44f5ac49fdb82e6447 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:37:14 +0100 Subject: [PATCH 14/18] unused import --- test/Permit2Passage.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Permit2Passage.t.sol b/test/Permit2Passage.t.sol index 854b82c..662e2ce 100644 --- a/test/Permit2Passage.t.sol +++ b/test/Permit2Passage.t.sol @@ -10,7 +10,7 @@ import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol" import {PermitHash} from "permit2/src/libraries/PermitHash.sol"; // other test utils -import {Permit2Helpers, Permit2Stub, BatchPermit2Stub, TestERC20} from "./Helpers.t.sol"; +import {Permit2Helpers, Permit2Stub, TestERC20} from "./Helpers.t.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {Test, console2} from "forge-std/Test.sol"; From 94f94af11f981f70cbf87879974a0002c2c4fc39 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:45:47 +0100 Subject: [PATCH 15/18] snapshot --- .gas-snapshot | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 0c824a4..55bf86d 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,7 +1,7 @@ -OrderOriginPermit2Test:test_fillPermit2() (gas: 98026) -OrderOriginPermit2Test:test_fillPermit2_multi() (gas: 903748) -OrderOriginPermit2Test:test_initiatePermit2() (gas: 138528) -OrderOriginPermit2Test:test_initiatePermit2_multi() (gas: 890825) +OrderOriginPermit2Test:test_fillPermit2() (gas: 104614) +OrderOriginPermit2Test:test_fillPermit2_multi() (gas: 905136) +OrderOriginPermit2Test:test_initiatePermit2() (gas: 145897) +OrderOriginPermit2Test:test_initiatePermit2_multi() (gas: 892213) OrdersTest:test_fill_ERC20() (gas: 70537) OrdersTest:test_fill_ETH() (gas: 68498) OrdersTest:test_fill_both() (gas: 166773) @@ -16,8 +16,8 @@ OrdersTest:test_orderExpired() (gas: 28106) OrdersTest:test_sweepERC20() (gas: 60491) OrdersTest:test_sweepETH() (gas: 82186) OrdersTest:test_underflowETH() (gas: 63690) -PassagePermit2Test:test_disallowedEnterPermit2() (gas: 669476) -PassagePermit2Test:test_enterTokenPermit2() (gas: 115926) +PassagePermit2Test:test_disallowedEnterPermit2() (gas: 666583) +PassagePermit2Test:test_enterTokenPermit2() (gas: 115862) PassageTest:test_configureEnter() (gas: 125771) PassageTest:test_disallowedEnter() (gas: 56619) PassageTest:test_enter() (gas: 25519) From de85ac2fcb5914959be98615b939961369dd43bd Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:51:22 +0100 Subject: [PATCH 16/18] add expectCall --- test/Permit2Orders.t.sol | 50 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/test/Permit2Orders.t.sol b/test/Permit2Orders.t.sol index 5b6528e..1afdf5a 100644 --- a/test/Permit2Orders.t.sol +++ b/test/Permit2Orders.t.sol @@ -97,12 +97,12 @@ contract OrderOriginPermit2Test is Permit2BatchTest { // sign permit + witness permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); + } + function test_initiatePermit2() public { // construct transfer details transferDetails.push(ISignatureTransfer.SignatureTransferDetails({to: tokenRecipient, requestedAmount: amount})); - } - function test_initiatePermit2() public { // expect Order event is initiated, ERC20 is transferred vm.expectEmit(); emit Order(deadline, inputs, outputs); @@ -124,6 +124,9 @@ contract OrderOriginPermit2Test is Permit2BatchTest { // input multiple ERC20s function test_initiatePermit2_multi() public { + // construct transfer details + transferDetails.push(ISignatureTransfer.SignatureTransferDetails({to: tokenRecipient, requestedAmount: amount})); + // setup second token address token2 = address(new TestERC20("bye", "BYE")); TestERC20(token2).mint(owner, amount * 10000); @@ -147,19 +150,49 @@ contract OrderOriginPermit2Test is Permit2BatchTest { // expect Order event is emitted, ERC20 is transferred vm.expectEmit(); emit Order(deadline, inputs, outputs); + vm.expectCall( + address(permit2Contract), + abi.encodeWithSelector( + BatchPermit2Stub.permitWitnessTransferFrom.selector, + permit2.permit, + transferDetails, + owner, + witness.witnessHash, + witness.witnessTypeString, + permit2.signature + ) + ); vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, tokenRecipient, amount)); vm.expectCall(token2, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, tokenRecipient, amount * 2)); target.initiatePermit2(tokenRecipient, outputs, permit2); } function test_fillPermit2() public { + // construct transfer details + transferDetails.push(ISignatureTransfer.SignatureTransferDetails({to: recipient, requestedAmount: amount})); + vm.expectEmit(); emit Filled(outputs); + vm.expectCall( + address(permit2Contract), + abi.encodeWithSelector( + BatchPermit2Stub.permitWitnessTransferFrom.selector, + permit2.permit, + transferDetails, + owner, + witness.witnessHash, + witness.witnessTypeString, + permit2.signature + ) + ); vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, recipient, amount)); target.fillPermit2(outputs, permit2); } function test_fillPermit2_multi() public { + // construct transfer details + transferDetails.push(ISignatureTransfer.SignatureTransferDetails({to: recipient, requestedAmount: amount})); + // setup second token address token2 = address(new TestERC20("bye", "BYE")); TestERC20(token2).mint(owner, amount * 10000); @@ -176,11 +209,24 @@ contract OrderOriginPermit2Test is Permit2BatchTest { transferDetails.push(ISignatureTransfer.SignatureTransferDetails({to: recipient, requestedAmount: amount * 2})); // re-sign new permit + witness = target.outputWitness(outputs); permit2.signature = signPermit(ownerKey, address(target), permit2.permit, witness); // expect Filled event is emitted, ERC20 is transferred vm.expectEmit(); emit Filled(outputs); + vm.expectCall( + address(permit2Contract), + abi.encodeWithSelector( + BatchPermit2Stub.permitWitnessTransferFrom.selector, + permit2.permit, + transferDetails, + owner, + witness.witnessHash, + witness.witnessTypeString, + permit2.signature + ) + ); vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, recipient, amount)); vm.expectCall(token2, abi.encodeWithSelector(ERC20.transferFrom.selector, owner, recipient, amount * 2)); target.fillPermit2(outputs, permit2); From 5c723e3d4d785a6b692c3990954b1c8888cd14ba Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 19:52:35 +0100 Subject: [PATCH 17/18] snapshot --- .gas-snapshot | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 55bf86d..6ab6b74 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,7 +1,7 @@ -OrderOriginPermit2Test:test_fillPermit2() (gas: 104614) -OrderOriginPermit2Test:test_fillPermit2_multi() (gas: 905136) -OrderOriginPermit2Test:test_initiatePermit2() (gas: 145897) -OrderOriginPermit2Test:test_initiatePermit2_multi() (gas: 892213) +OrderOriginPermit2Test:test_fillPermit2() (gas: 194206) +OrderOriginPermit2Test:test_fillPermit2_multi() (gas: 984133) +OrderOriginPermit2Test:test_initiatePermit2() (gas: 206669) +OrderOriginPermit2Test:test_initiatePermit2_multi() (gas: 961900) OrdersTest:test_fill_ERC20() (gas: 70537) OrdersTest:test_fill_ETH() (gas: 68498) OrdersTest:test_fill_both() (gas: 166773) From beca65adacb84cd0f938b6de7dacb25d39f98b5e Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 19 Jul 2024 20:20:29 +0100 Subject: [PATCH 18/18] feat: redo permit tests as mainnet fork --- .gas-snapshot | 14 ++--- test/Helpers.t.sol | 125 ++++++++++++++++++++++++++------------ test/Permit2Orders.t.sol | 40 +++--------- test/Permit2Passage.t.sol | 35 +++-------- 4 files changed, 112 insertions(+), 102 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 6ab6b74..053a379 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,7 +1,7 @@ -OrderOriginPermit2Test:test_fillPermit2() (gas: 194206) -OrderOriginPermit2Test:test_fillPermit2_multi() (gas: 984133) -OrderOriginPermit2Test:test_initiatePermit2() (gas: 206669) -OrderOriginPermit2Test:test_initiatePermit2_multi() (gas: 961900) +OrderOriginPermit2Test:test_fillPermit2() (gas: 225289) +OrderOriginPermit2Test:test_fillPermit2_multi() (gas: 1019134) +OrderOriginPermit2Test:test_initiatePermit2() (gas: 235752) +OrderOriginPermit2Test:test_initiatePermit2_multi() (gas: 989274) OrdersTest:test_fill_ERC20() (gas: 70537) OrdersTest:test_fill_ETH() (gas: 68498) OrdersTest:test_fill_both() (gas: 166773) @@ -16,8 +16,8 @@ OrdersTest:test_orderExpired() (gas: 28106) OrdersTest:test_sweepERC20() (gas: 60491) OrdersTest:test_sweepETH() (gas: 82186) OrdersTest:test_underflowETH() (gas: 63690) -PassagePermit2Test:test_disallowedEnterPermit2() (gas: 666583) -PassagePermit2Test:test_enterTokenPermit2() (gas: 115862) +PassagePermit2Test:test_disallowedEnterPermit2() (gas: 699630) +PassagePermit2Test:test_enterTokenPermit2() (gas: 145449) PassageTest:test_configureEnter() (gas: 125771) PassageTest:test_disallowedEnter() (gas: 56619) PassageTest:test_enter() (gas: 25519) @@ -29,7 +29,7 @@ PassageTest:test_onlyTokenAdmin() (gas: 16881) PassageTest:test_receive() (gas: 21383) PassageTest:test_setUp() (gas: 17011) PassageTest:test_withdraw() (gas: 59188) -RollupPassagePermit2Test:test_exitTokenPermit2() (gas: 99802) +RollupPassagePermit2Test:test_exitTokenPermit2() (gas: 129402) RollupPassageTest:test_exit() (gas: 22403) RollupPassageTest:test_exitToken() (gas: 50232) RollupPassageTest:test_fallback() (gas: 19949) diff --git a/test/Helpers.t.sol b/test/Helpers.t.sol index 1453adf..ef5e73d 100644 --- a/test/Helpers.t.sol +++ b/test/Helpers.t.sol @@ -17,38 +17,23 @@ contract TestERC20 is ERC20Burnable { } } -contract Permit2Stub { - /// @notice stubbed `permitWitnessTransferFrom` - does not check signature, nonce, or deadline - function permitWitnessTransferFrom( - ISignatureTransfer.PermitTransferFrom memory permit, - ISignatureTransfer.SignatureTransferDetails calldata transferDetails, - address owner, - bytes32, /*witness*/ - string calldata, /*witnessTypeString*/ - bytes calldata /*signature*/ - ) external { - ERC20(permit.permitted.token).transferFrom(owner, transferDetails.to, transferDetails.requestedAmount); - } -} +contract Permit2Helpers is Test { + address permit2Contract; -contract BatchPermit2Stub { - function permitWitnessTransferFrom( - ISignatureTransfer.PermitBatchTransferFrom memory permit, - ISignatureTransfer.SignatureTransferDetails[] calldata transferDetails, - address owner, - bytes32, /*witness*/ - string calldata, /*witnessTypeString*/ - bytes calldata /*signature*/ - ) external { - for (uint256 i = 0; i < transferDetails.length; i++) { - ERC20(permit.permitted[i].token).transferFrom( - owner, transferDetails[i].to, transferDetails[i].requestedAmount - ); - } - } -} + /// @notice the address signing the Permit messages and its pk + uint256 ownerKey = 123; + address owner = vm.addr(ownerKey); + + // permit consts + UsesPermit2.Witness witness; + + bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; + uint256 private immutable _CACHED_CHAIN_ID; + + bytes32 private constant _HASHED_NAME = keccak256("Permit2"); + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); -contract Permit2Helpers is Test { string public constant _PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB = "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,"; @@ -57,18 +42,38 @@ contract Permit2Helpers is Test { string public constant _PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB = "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,"; + // Cache the domain separator as an immutable value, but also store the chain id that it + // corresponds to, in order to invalidate the cached domain separator if the chain id changes. + constructor() { + _CACHED_CHAIN_ID = block.chainid; + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME); + } + + function _setUpPermit2(address token, uint256 amount) internal { + vm.label(owner, "owner"); + + // setup permit2 contract + permit2Contract = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + vm.label(address(permit2Contract), "permit2"); + + // approve permit2 + vm.prank(owner); + TestERC20(token).approve(address(permit2Contract), amount * 10000); + } + /// @notice given a Permit and a Witness, produce a signature from the `owner` function signPermit( uint256 signingKey, address spender, ISignatureTransfer.PermitTransferFrom memory permit, UsesPermit2.Witness memory _witness - ) internal pure returns (bytes memory signature) { + ) internal view returns (bytes memory signature) { bytes32 permit2Hash = hashWithWitness(spender, permit, _witness.witnessHash, _witness.witnessTypeString); + bytes32 signHash = _hashTypedData(permit2Hash); uint8 v; bytes32 r; bytes32 s; - (v, r, s) = vm.sign(signingKey, permit2Hash); + (v, r, s) = vm.sign(signingKey, signHash); signature = abi.encodePacked(r, s, v); } @@ -76,13 +81,13 @@ contract Permit2Helpers is Test { function hashWithWitness( address spender, ISignatureTransfer.PermitTransferFrom memory _permit, - bytes32 witness, + bytes32 _witness, string memory witnessTypeString ) internal pure returns (bytes32) { bytes32 typeHash = keccak256(abi.encodePacked(_PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, witnessTypeString)); bytes32 tokenPermissionsHash = _hashTokenPermissions(_permit.permitted); - return keccak256(abi.encode(typeHash, tokenPermissionsHash, spender, _permit.nonce, _permit.deadline, witness)); + return keccak256(abi.encode(typeHash, tokenPermissionsHash, spender, _permit.nonce, _permit.deadline, _witness)); } /// @notice given a Permit and a Witness, produce a signature from the `owner` @@ -91,19 +96,20 @@ contract Permit2Helpers is Test { address spender, ISignatureTransfer.PermitBatchTransferFrom memory permit, UsesPermit2.Witness memory _witness - ) internal pure returns (bytes memory signature) { + ) internal view returns (bytes memory signature) { bytes32 permit2Hash = hashWithWitness(spender, permit, _witness.witnessHash, _witness.witnessTypeString); + bytes32 signHash = _hashTypedData(permit2Hash); uint8 v; bytes32 r; bytes32 s; - (v, r, s) = vm.sign(signingKey, permit2Hash); + (v, r, s) = vm.sign(signingKey, signHash); signature = abi.encodePacked(r, s, v); } function hashWithWitness( address spender, ISignatureTransfer.PermitBatchTransferFrom memory permit, - bytes32 witness, + bytes32 _witness, string memory witnessTypeString ) internal pure returns (bytes32) { bytes32 typeHash = @@ -123,7 +129,7 @@ contract Permit2Helpers is Test { spender, permit.nonce, permit.deadline, - witness + _witness ) ); } @@ -136,6 +142,49 @@ contract Permit2Helpers is Test { { return keccak256(abi.encode(_TOKEN_PERMISSIONS_TYPEHASH, _permitted)); } + + /// @notice Returns the domain separator for the current chain. + /// @dev Uses cached version if chainid and address are unchanged from construction. + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return block.chainid == _CACHED_CHAIN_ID + ? _CACHED_DOMAIN_SEPARATOR + : _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME); + } + + /// @notice Builds a domain separator using the current chainId and contract address. + function _buildDomainSeparator(bytes32 typeHash, bytes32 nameHash) private view returns (bytes32) { + return keccak256(abi.encode(typeHash, nameHash, block.chainid, permit2Contract)); + } + + /// @notice Creates an EIP-712 typed data hash + function _hashTypedData(bytes32 dataHash) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), dataHash)); + } +} + +/// HACK to use abi.encodeWithSelector +interface ISinglePermit { + /// @notice stubbed `permitWitnessTransferFrom` - does not check signature, nonce, or deadline + function permitWitnessTransferFrom( + ISignatureTransfer.PermitTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails calldata transferDetails, + address owner, + bytes32, /*witness*/ + string calldata, /*witnessTypeString*/ + bytes calldata /*signature*/ + ) external; +} + +/// HACK to use abi.encodeWithSelector +interface IBatchPermit { + function permitWitnessTransferFrom( + ISignatureTransfer.PermitBatchTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes32, /*witness*/ + string calldata, /*witnessTypeString*/ + bytes calldata /*signature*/ + ) external; } contract HelpersTest is Test { diff --git a/test/Permit2Orders.t.sol b/test/Permit2Orders.t.sol index 1afdf5a..4d9d04e 100644 --- a/test/Permit2Orders.t.sol +++ b/test/Permit2Orders.t.sol @@ -8,41 +8,17 @@ import {UsesPermit2} from "../src/permit2/UsesPermit2.sol"; // Permit2 deps import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; -import {PermitHash} from "permit2/src/libraries/PermitHash.sol"; // other test utils -import {Permit2Helpers, BatchPermit2Stub, TestERC20} from "./Helpers.t.sol"; +import {Permit2Helpers, IBatchPermit, TestERC20} from "./Helpers.t.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {Test, console2} from "forge-std/Test.sol"; contract Permit2BatchTest is Permit2Helpers { - BatchPermit2Stub permit2Contract; - - /// @notice the address signing the Permit messages and its pk - uint256 ownerKey = 123; - address owner = vm.addr(ownerKey); - - // permit consts - UsesPermit2.Witness witness; // batch permit UsesPermit2.Permit2Batch permit2; ISignatureTransfer.SignatureTransferDetails[] transferDetails; - function _setUpPermit2(address token, uint256 amount) internal { - vm.label(owner, "owner"); - - // deploy batch permit2 - permit2Contract = new BatchPermit2Stub(); - vm.label(address(permit2Contract), "permit2"); - - // approve batch permit2 - vm.prank(owner); - TestERC20(token).approve(address(permit2Contract), amount * 10000); - - _setupBatchPermit(token, amount); - } - function _setupBatchPermit(address token, uint256 amount) internal { // create a batch permit with generic details permit2.permit.permitted.push(ISignatureTransfer.TokenPermissions({token: token, amount: amount})); @@ -73,6 +49,7 @@ contract OrderOriginPermit2Test is Permit2BatchTest { event Filled(IOrders.Output[] outputs); function setUp() public { + vm.createSelectFork("https://ethereum-rpc.publicnode.com"); // deploy token token = address(new TestERC20("hi", "HI")); TestERC20(token).mint(owner, amount * 10000); @@ -80,6 +57,7 @@ contract OrderOriginPermit2Test is Permit2BatchTest { // setup permit2 contract & permit details _setUpPermit2(token, amount); + _setupBatchPermit(token, amount); // deploy Orders contract target = new RollupOrders(address(permit2Contract)); @@ -105,11 +83,11 @@ contract OrderOriginPermit2Test is Permit2BatchTest { // expect Order event is initiated, ERC20 is transferred vm.expectEmit(); - emit Order(deadline, inputs, outputs); + emit Order(permit2.permit.deadline, inputs, outputs); vm.expectCall( address(permit2Contract), abi.encodeWithSelector( - BatchPermit2Stub.permitWitnessTransferFrom.selector, + IBatchPermit.permitWitnessTransferFrom.selector, permit2.permit, transferDetails, owner, @@ -149,11 +127,11 @@ contract OrderOriginPermit2Test is Permit2BatchTest { // expect Order event is emitted, ERC20 is transferred vm.expectEmit(); - emit Order(deadline, inputs, outputs); + emit Order(permit2.permit.deadline, inputs, outputs); vm.expectCall( address(permit2Contract), abi.encodeWithSelector( - BatchPermit2Stub.permitWitnessTransferFrom.selector, + IBatchPermit.permitWitnessTransferFrom.selector, permit2.permit, transferDetails, owner, @@ -176,7 +154,7 @@ contract OrderOriginPermit2Test is Permit2BatchTest { vm.expectCall( address(permit2Contract), abi.encodeWithSelector( - BatchPermit2Stub.permitWitnessTransferFrom.selector, + IBatchPermit.permitWitnessTransferFrom.selector, permit2.permit, transferDetails, owner, @@ -218,7 +196,7 @@ contract OrderOriginPermit2Test is Permit2BatchTest { vm.expectCall( address(permit2Contract), abi.encodeWithSelector( - BatchPermit2Stub.permitWitnessTransferFrom.selector, + IBatchPermit.permitWitnessTransferFrom.selector, permit2.permit, transferDetails, owner, diff --git a/test/Permit2Passage.t.sol b/test/Permit2Passage.t.sol index 662e2ce..6fd6df0 100644 --- a/test/Permit2Passage.t.sol +++ b/test/Permit2Passage.t.sol @@ -7,41 +7,18 @@ import {UsesPermit2} from "../src/permit2/UsesPermit2.sol"; // Permit2 deps import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; -import {PermitHash} from "permit2/src/libraries/PermitHash.sol"; // other test utils -import {Permit2Helpers, Permit2Stub, TestERC20} from "./Helpers.t.sol"; +import {Permit2Helpers, ISinglePermit, TestERC20} from "./Helpers.t.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {Test, console2} from "forge-std/Test.sol"; contract SharedPermit2Test is Permit2Helpers { - Permit2Stub permit2Contract; - - /// @notice the address signing the Permit messages and its pk - uint256 ownerKey = 123; - address owner = vm.addr(ownerKey); - - // permit consts - UsesPermit2.Witness witness; // single permit UsesPermit2.Permit2 permit2; ISignatureTransfer.SignatureTransferDetails transferDetails; - function _setUpPermit2(address token, uint256 amount) internal { - vm.label(owner, "owner"); - - // deploy permit2 - permit2Contract = new Permit2Stub(); - vm.label(address(permit2Contract), "permit2"); - - // approve permit2 - vm.prank(owner); - TestERC20(token).approve(address(permit2Contract), amount * 10000); - - _setupSinglePermit(token, amount); - } - function _setupSinglePermit(address token, uint256 amount) internal { // create a single permit with generic details permit2.permit = ISignatureTransfer.PermitTransferFrom({ @@ -67,6 +44,8 @@ contract PassagePermit2Test is SharedPermit2Test { ); function setUp() public { + vm.createSelectFork("https://ethereum-rpc.publicnode.com"); + // deploy token token = address(new TestERC20("hi", "HI")); TestERC20(token).mint(owner, amount * 10000); @@ -77,6 +56,7 @@ contract PassagePermit2Test is SharedPermit2Test { // setup permit2 contract & permit details _setUpPermit2(token, amount); + _setupSinglePermit(token, amount); // deploy Passage target = new Passage(block.chainid + 1, address(this), initialEnterTokens, address(permit2Contract)); @@ -98,7 +78,7 @@ contract PassagePermit2Test is SharedPermit2Test { vm.expectCall( address(permit2Contract), abi.encodeWithSelector( - Permit2Stub.permitWitnessTransferFrom.selector, + ISinglePermit.permitWitnessTransferFrom.selector, permit2.permit, transferDetails, owner, @@ -142,12 +122,15 @@ contract RollupPassagePermit2Test is SharedPermit2Test { event ExitToken(address indexed hostRecipient, address indexed token, uint256 amount); function setUp() public { + vm.createSelectFork("https://ethereum-rpc.publicnode.com"); + // deploy token & approve permit2 token = address(new TestERC20("hi", "HI")); TestERC20(token).mint(owner, amount * 10000); // setup permit2 contract & permit details _setUpPermit2(token, amount); + _setupSinglePermit(token, amount); // deploy Passage target = new RollupPassage(address(permit2Contract)); @@ -169,7 +152,7 @@ contract RollupPassagePermit2Test is SharedPermit2Test { vm.expectCall( address(permit2Contract), abi.encodeWithSelector( - Permit2Stub.permitWitnessTransferFrom.selector, + ISinglePermit.permitWitnessTransferFrom.selector, permit2.permit, transferDetails, owner,