Skip to content

Commit

Permalink
feat: permit2 for token flows (#63)
Browse files Browse the repository at this point in the history
* feat: permit2 for token flows

* add permit2 as submodule, organize files to folders (#65)

* forge install: permit2

* remove vendored contracts

* move IOrders to interfaces/

* move permit2 to folder

* break permit2 functionality into discrete contracts

* fix: update witness encoding for EIP-712 compliance

* refactor: generate witness as public field

* minor refactor

* snapshot

* function visibility & ordering

* test: permit2 flows

* snapshot

* remove TODOs

* split batch and single helpers

* split up passage/orders tests

* unused import

* snapshot

* add expectCall

* snapshot

* feat: redo permit tests as mainnet fork

---------

Co-authored-by: James Prestwich <[email protected]>
  • Loading branch information
anna-carroll and prestwich authored Jul 20, 2024
1 parent 9e8e460 commit 3cbe2fe
Show file tree
Hide file tree
Showing 17 changed files with 986 additions and 130 deletions.
66 changes: 36 additions & 30 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,39 +1,45 @@
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)
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)
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)
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)
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)
PassageTest:test_enterToken() (gas: 64397)
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: 21339)
PassageTest:test_setUp() (gas: 16901)
PassageTest:test_receive() (gas: 21383)
PassageTest:test_setUp() (gas: 17011)
PassageTest:test_withdraw() (gas: 59188)
RollupPassageTest:test_exit() (gas: 22347)
RollupPassageTest:test_exitToken() (gas: 50183)
RollupPassageTest:test_fallback() (gas: 19883)
RollupPassagePermit2Test:test_exitTokenPermit2() (gas: 129402)
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)
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions lib/permit2
Submodule permit2 added at cc56ad
17 changes: 9 additions & 8 deletions script/Zenith.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
132 changes: 79 additions & 53 deletions src/Orders.sol
Original file line number Diff line number Diff line change
@@ -1,46 +1,51 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {OrdersPermit2, UsesPermit2} from "./permit2/UsesPermit2.sol";
import {IOrders} from "./interfaces/IOrders.sol";
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;
}

/// @notice Contract capable of processing fulfillment of intent-based Orders.
abstract contract OrderDestination {
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);

/// @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 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, OrdersPermit2.Permit2Batch calldata permit2) external {
// transfer all tokens to the Output recipients via permit2 (includes check on nonce & deadline)
_permitWitnessTransferFrom(
outputWitness(outputs), _fillTransferDetails(outputs, permit2.permit.permitted), permit2
);

// 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)) {
Expand All @@ -51,19 +56,14 @@ abstract contract OrderDestination {
IERC20(outputs[i].token).transferFrom(msg.sender, outputs[i].recipient, outputs[i].amount);
}
}
// emit
emit Filled(outputs);
}
}

/// @notice Contract capable of registering initiation of intent-based Orders.
abstract contract OrderOrigin {
abstract contract OrderOrigin is IOrders, OrdersPermit2 {
/// @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);
Expand All @@ -73,14 +73,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.
Expand All @@ -97,17 +98,26 @@ 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.
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.
/// @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,
OrdersPermit2.Permit2Batch calldata permit2
) external {
// transfer all tokens to the tokenRecipient via permit2 (includes check on nonce & deadline)
_permitWitnessTransferFrom(
outputWitness(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.
Expand All @@ -118,8 +128,7 @@ abstract contract OrderOrigin {
/// @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 {
if (msg.sender != block.coinbase) revert OnlyBuilder();
function sweep(address recipient, address token) external {
// send ETH or tokens
uint256 balance;
if (token == address(0)) {
Expand All @@ -131,8 +140,25 @@ abstract contract OrderOrigin {
}
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 {}
contract HostOrders is OrderDestination {
constructor(address _permit2) UsesPermit2(_permit2) {}
}

contract RollupOrders is OrderOrigin, OrderDestination {}
contract RollupOrders is OrderOrigin, OrderDestination {
constructor(address _permit2) UsesPermit2(_permit2) {}
}
Loading

0 comments on commit 3cbe2fe

Please sign in to comment.