Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: permit2 for token flows #63

Merged
merged 19 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 25 additions & 26 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -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)
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
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
28 changes: 28 additions & 0 deletions src/IOrders.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
98 changes: 58 additions & 40 deletions src/Orders.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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);
Expand All @@ -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.
Expand All @@ -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++) {
Expand All @@ -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`.
Expand All @@ -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)) {
Expand All @@ -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) {}
}
Loading