Skip to content

Commit ff795e8

Browse files
committed
feat: permit2 for token flows
1 parent 9e8e460 commit ff795e8

File tree

12 files changed

+496
-106
lines changed

12 files changed

+496
-106
lines changed

.gas-snapshot

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
1-
OrdersTest:test_fill_ERC20() (gas: 70364)
2-
OrdersTest:test_fill_ETH() (gas: 68414)
3-
OrdersTest:test_fill_both() (gas: 166580)
4-
OrdersTest:test_fill_multiETH() (gas: 131926)
5-
OrdersTest:test_fill_underflowETH() (gas: 115281)
6-
OrdersTest:test_initiate_ERC20() (gas: 81435)
7-
OrdersTest:test_initiate_ETH() (gas: 44949)
8-
OrdersTest:test_initiate_both() (gas: 118677)
9-
OrdersTest:test_initiate_multiERC20() (gas: 722408)
10-
OrdersTest:test_initiate_multiETH() (gas: 75304)
11-
OrdersTest:test_onlyBuilder() (gas: 12815)
12-
OrdersTest:test_orderExpired() (gas: 27956)
13-
OrdersTest:test_sweepERC20() (gas: 60446)
14-
OrdersTest:test_sweepETH() (gas: 81940)
15-
OrdersTest:test_underflowETH() (gas: 63528)
16-
PassageTest:test_configureEnter() (gas: 82311)
17-
PassageTest:test_disallowedEnter() (gas: 17938)
1+
OrdersTest:test_fill_ERC20() (gas: 70515)
2+
OrdersTest:test_fill_ETH() (gas: 68476)
3+
OrdersTest:test_fill_both() (gas: 166751)
4+
OrdersTest:test_fill_multiETH() (gas: 132097)
5+
OrdersTest:test_fill_underflowETH() (gas: 115381)
6+
OrdersTest:test_initiate_ERC20() (gas: 81614)
7+
OrdersTest:test_initiate_ETH() (gas: 45128)
8+
OrdersTest:test_initiate_both() (gas: 118889)
9+
OrdersTest:test_initiate_multiERC20() (gas: 722620)
10+
OrdersTest:test_initiate_multiETH() (gas: 75516)
11+
OrdersTest:test_orderExpired() (gas: 28084)
12+
OrdersTest:test_sweepERC20() (gas: 60469)
13+
OrdersTest:test_sweepETH() (gas: 82142)
14+
OrdersTest:test_underflowETH() (gas: 63668)
15+
PassageTest:test_configureEnter() (gas: 125672)
16+
PassageTest:test_disallowedEnter() (gas: 56597)
1817
PassageTest:test_enter() (gas: 25507)
19-
PassageTest:test_enterToken() (gas: 64354)
20-
PassageTest:test_enterToken_defaultChain() (gas: 62870)
21-
PassageTest:test_enter_defaultChain() (gas: 24011)
22-
PassageTest:test_fallback() (gas: 21445)
18+
PassageTest:test_enterToken() (gas: 64397)
19+
PassageTest:test_enterToken_defaultChain() (gas: 62935)
20+
PassageTest:test_enter_defaultChain() (gas: 24033)
21+
PassageTest:test_fallback() (gas: 21489)
2322
PassageTest:test_onlyTokenAdmin() (gas: 16881)
24-
PassageTest:test_receive() (gas: 21339)
25-
PassageTest:test_setUp() (gas: 16901)
23+
PassageTest:test_receive() (gas: 21361)
24+
PassageTest:test_setUp() (gas: 16923)
2625
PassageTest:test_withdraw() (gas: 59188)
27-
RollupPassageTest:test_exit() (gas: 22347)
28-
RollupPassageTest:test_exitToken() (gas: 50183)
29-
RollupPassageTest:test_fallback() (gas: 19883)
26+
RollupPassageTest:test_exit() (gas: 22369)
27+
RollupPassageTest:test_exitToken() (gas: 50214)
28+
RollupPassageTest:test_fallback() (gas: 19905)
3029
RollupPassageTest:test_receive() (gas: 19844)
3130
TransactTest:test_configureGas() (gas: 22828)
3231
TransactTest:test_enterTransact() (gas: 103961)

.github/workflows/cd.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ jobs:
5050
environment: dev
5151
forge-deployment-contract: ZenithScript
5252
forge-deployment-script-file: Zenith.s.sol
53-
forge-deployment-signature: "deploy(uint256,address,address[],address)"
54-
forge-deployment-params: "17001 0x11Aa4EBFbf7a481617c719a2Df028c9DA1a219aa [] 0x29403F107781ea45Bf93710abf8df13F67f2008f"
53+
forge-deployment-signature: "deploy(uint256,address,address[],address,address)"
54+
forge-deployment-params: "17001 0x11Aa4EBFbf7a481617c719a2Df028c9DA1a219aa [] 0x29403F107781ea45Bf93710abf8df13F67f2008f 0x000000000022D473030F116dDEE9F6B43aC78BA3"
5555
etherscan-url: https://holesky.etherscan.io
5656
chain-id: 17000
5757
deployer-address: ${{ vars.HOLESKY_DEPLOYER_ADDRESS }}

script/Zenith.s.sol

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,27 @@ import {HostOrders, RollupOrders} from "../src/Orders.sol";
99

1010
contract ZenithScript is Script {
1111
// deploy:
12-
// 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
12+
// 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
1313
function deploy(
1414
uint256 defaultRollupChainId,
1515
address withdrawalAdmin,
1616
address[] memory initialEnterTokens,
17-
address sequencerAndGasAdmin
17+
address sequencerAndGasAdmin,
18+
address permit2
1819
) public returns (Zenith z, Passage p, Transactor t, HostOrders m) {
1920
vm.startBroadcast();
2021
z = new Zenith(sequencerAndGasAdmin);
21-
p = new Passage(defaultRollupChainId, withdrawalAdmin, initialEnterTokens);
22+
p = new Passage(defaultRollupChainId, withdrawalAdmin, initialEnterTokens, permit2);
2223
t = new Transactor(defaultRollupChainId, sequencerAndGasAdmin, p, 30_000_000, 5_000_000);
23-
m = new HostOrders();
24+
m = new HostOrders(permit2);
2425
}
2526

2627
// deploy:
27-
// forge script ZenithScript --sig "deployL2()" --rpc-url $L2_RPC_URL --private-key $PRIVATE_KEY --broadcast
28-
function deployL2() public returns (RollupPassage p, RollupOrders m) {
28+
// forge script ZenithScript --sig "deployL2(address)" --rpc-url $L2_RPC_URL --private-key $PRIVATE_KEY --broadcast $PERMIT_2
29+
function deployL2(address permit2) public returns (RollupPassage p, RollupOrders m) {
2930
vm.startBroadcast();
30-
p = new RollupPassage();
31-
m = new RollupOrders();
31+
p = new RollupPassage(permit2);
32+
m = new RollupOrders(permit2);
3233
}
3334

3435
// NOTE: script must be run using SequencerAdmin key

src/IOrders.sol

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.24;
3+
4+
interface IOrders {
5+
/// @notice Tokens sent by the swapper as inputs to the order
6+
/// @dev From ERC-7683
7+
struct Input {
8+
/// @dev The address of the ERC20 token on the origin chain
9+
address token;
10+
/// @dev The amount of the token to be sent
11+
uint256 amount;
12+
}
13+
14+
/// @notice Tokens that must be receive for a valid order fulfillment
15+
/// @dev From ERC-7683
16+
struct Output {
17+
/// @dev The address of the ERC20 token on the destination chain
18+
/// @dev address(0) used as a sentinel for the native token
19+
address token;
20+
/// @dev The amount of the token to be sent
21+
uint256 amount;
22+
/// @dev The address to receive the output tokens
23+
address recipient;
24+
/// @dev When emitted on the origin chain, the destination chain for the Output.
25+
/// When emitted on the destination chain, the origin chain for the Order containing the Output.
26+
uint32 chainId;
27+
}
28+
}

src/Orders.sol

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,30 @@
22
pragma solidity ^0.8.24;
33

44
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
5-
6-
/// @notice Tokens sent by the swapper as inputs to the order
7-
/// @dev From ERC-7683
8-
struct Input {
9-
/// @dev The address of the ERC20 token on the origin chain
10-
address token;
11-
/// @dev The amount of the token to be sent
12-
uint256 amount;
13-
}
14-
15-
/// @notice Tokens that must be receive for a valid order fulfillment
16-
/// @dev From ERC-7683
17-
struct Output {
18-
/// @dev The address of the ERC20 token on the destination chain
19-
/// @dev address(0) used as a sentinel for the native token
20-
address token;
21-
/// @dev The amount of the token to be sent
22-
uint256 amount;
23-
/// @dev The address to receive the output tokens
24-
address recipient;
25-
/// @dev When emitted on the origin chain, the destination chain for the Output.
26-
/// When emitted on the destination chain, the origin chain for the Order containing the Output.
27-
uint32 chainId;
28-
}
5+
import {Permit2Batch, UsesPermit2} from "./UsesPermit2.sol";
6+
import {IOrders} from "./IOrders.sol";
297

308
/// @notice Contract capable of processing fulfillment of intent-based Orders.
31-
abstract contract OrderDestination {
9+
abstract contract OrderDestination is IOrders, UsesPermit2 {
3210
/// @notice Emitted when Order Outputs are sent to their recipients.
3311
/// @dev NOTE that here, Output.chainId denotes the *origin* chainId.
3412
event Filled(Output[] outputs);
3513

36-
/// @notice Send the Output(s) of any number of Orders.
37-
/// The user calls `initiate` on a rollup; the Builder calls `fill` on the target chain aggregating Outputs.
38-
/// Builder may aggregate multiple Outputs with the same (`chainId`, `recipient`, `token`) into a single Output with the summed `amount`.
14+
/// @notice Fill any number of Order(s), by transferring their Output(s).
15+
/// @dev Filler may aggregate multiple Outputs with the same (`chainId`, `recipient`, `token`) into a single Output with the summed `amount`.
3916
/// @dev NOTE that here, Output.chainId denotes the *origin* chainId.
4017
/// @param outputs - The Outputs to be transferred.
4118
/// @custom:emits Filled
4219
function fill(Output[] memory outputs) external payable {
4320
// transfer outputs
21+
_transferOutputs(outputs);
22+
23+
// emit
24+
emit Filled(outputs);
25+
}
26+
27+
/// @notice Transfer the Order Outputs to their recipients.
28+
function _transferOutputs(Output[] memory outputs) internal {
4429
uint256 value = msg.value;
4530
for (uint256 i; i < outputs.length; i++) {
4631
if (outputs[i].token == address(0)) {
@@ -51,19 +36,32 @@ abstract contract OrderDestination {
5136
IERC20(outputs[i].token).transferFrom(msg.sender, outputs[i].recipient, outputs[i].amount);
5237
}
5338
}
39+
}
40+
41+
/// @notice Fill any number of Order(s), by transferring their Output(s) via permit2 signed batch transfer.
42+
/// @dev Can only provide ERC20 tokens as Outputs.
43+
/// @dev Filler may aggregate multiple Outputs with the same (`chainId`, `recipient`, `token`) into a single Output with the summed `amount`.
44+
/// @dev the permit2 signer is the Filler providing the Outputs.
45+
/// @dev the permit2 `permitted` tokens MUST match provided Outputs.
46+
/// @dev Filler MUST submit `fill` and `intitiate` within an atomic bundle.
47+
/// @dev NOTE that here, Output.chainId denotes the *origin* chainId.
48+
/// @param outputs - The Outputs to be transferred. signed over via permit2 witness.
49+
/// @param permit2 - the permit2 details, signer, and signature.
50+
/// @custom:emits Filled
51+
function fillPermit2(Output[] memory outputs, Permit2Batch calldata permit2) external {
52+
// transfer all tokens to the Output recipients via permit2 (includes check on nonce & deadline)
53+
_permitWitnessTransferFrom(outputs, _fillTransferDetails(outputs, permit2.permit.permitted), permit2);
54+
5455
// emit
5556
emit Filled(outputs);
5657
}
5758
}
5859

5960
/// @notice Contract capable of registering initiation of intent-based Orders.
60-
abstract contract OrderOrigin {
61+
abstract contract OrderOrigin is IOrders, UsesPermit2 {
6162
/// @notice Thrown when an Order is submitted with a deadline that has passed.
6263
error OrderExpired();
6364

64-
/// @notice Thrown when trying to call `sweep` if not the Builder of the block.
65-
error OnlyBuilder();
66-
6765
/// @notice Emitted when an Order is submitted for fulfillment.
6866
/// @dev NOTE that here, Output.chainId denotes the *destination* chainId.
6967
event Order(uint256 deadline, Input[] inputs, Output[] outputs);
@@ -73,14 +71,15 @@ abstract contract OrderOrigin {
7371
/// Intentionally does not bother to emit which token(s) were swept, nor their amounts.
7472
event Sweep(address indexed recipient, address indexed token, uint256 amount);
7573

76-
/// @notice Request to swap ERC20s.
74+
/// @notice Initiate an Order.
75+
/// @dev Filler MUST submit `fill` and `intitiate` + `sweep` within an atomic bundle.
76+
/// @dev NOTE that here, Output.chainId denotes the *target* chainId.
7777
/// @dev inputs are provided on the rollup; in exchange,
7878
/// outputs are expected to be received on the target chain(s).
79-
/// @dev Fees paid to the Builders for fulfilling the Orders
80-
/// can be included within the "exchange rate" between inputs and outputs.
81-
/// @dev The Builder claims the inputs from the contract by submitting `sweep` transactions within the same block.
8279
/// @dev The Rollup STF MUST NOT apply `initiate` transactions to the rollup state
8380
/// UNLESS the outputs are delivered on the target chains within the same block.
81+
/// @dev Fees paid to the Builders for fulfilling the Orders
82+
/// can be included within the "exchange rate" between inputs and outputs.
8483
/// @param deadline - The deadline at or before which the Order must be fulfilled.
8584
/// @param inputs - The token amounts offered by the swapper in exchange for the outputs.
8685
/// @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 {
9796
emit Order(deadline, inputs, outputs);
9897
}
9998

100-
/// @notice Transfer the Order inputs to this contract, where they can be collected by the Order filler.
99+
/// @notice Transfer the Order inputs to this contract, where they can be collected by the Order filler via `sweep`.
101100
function _transferInputs(Input[] memory inputs) internal {
102101
uint256 value = msg.value;
103102
for (uint256 i; i < inputs.length; i++) {
@@ -110,6 +109,22 @@ abstract contract OrderOrigin {
110109
}
111110
}
112111

112+
/// @notice Initiate an Order, transferring Input tokens to the Filler via permit2 signed batch transfer.
113+
/// @dev Can only provide ERC20 tokens as Inputs.
114+
/// @dev the permit2 signer is the swapper providing the Input tokens in exchange for the Outputs.
115+
/// @dev Filler MUST submit `fill` and `intitiate` within an atomic bundle.
116+
/// @dev NOTE that here, Output.chainId denotes the *target* chainId.
117+
/// @param tokenRecipient - the recipient of the Input tokens, provided by msg.sender (un-verified by permit2).
118+
/// @param outputs - the Outputs required in exchange for the Input tokens. signed over via permit2 witness.
119+
/// @param permit2 - the permit2 details, signer, and signature.
120+
function initiatePermit2(address tokenRecipient, Output[] memory outputs, Permit2Batch calldata permit2) external {
121+
// transfer all tokens to the tokenRecipient via permit2 (includes check on nonce & deadline)
122+
_permitWitnessTransferFrom(outputs, _initiateTransferDetails(tokenRecipient, permit2.permit.permitted), permit2);
123+
124+
// emit
125+
emit Order(permit2.permit.deadline, _inputs(permit2.permit.permitted), outputs);
126+
}
127+
113128
/// @notice Transfer the entire balance of ERC20 tokens to the recipient.
114129
/// @dev Called by the Builder within the same block as users' `initiate` transactions
115130
/// to claim the `inputs`.
@@ -119,7 +134,6 @@ abstract contract OrderOrigin {
119134
/// @custom:emits Sweep
120135
/// @custom:reverts OnlyBuilder if called by non-block builder
121136
function sweep(address recipient, address token) public {
122-
if (msg.sender != block.coinbase) revert OnlyBuilder();
123137
// send ETH or tokens
124138
uint256 balance;
125139
if (token == address(0)) {
@@ -133,6 +147,10 @@ abstract contract OrderOrigin {
133147
}
134148
}
135149

136-
contract HostOrders is OrderDestination {}
150+
contract HostOrders is OrderDestination {
151+
constructor(address _permit2) UsesPermit2(_permit2) {}
152+
}
137153

138-
contract RollupOrders is OrderOrigin, OrderDestination {}
154+
contract RollupOrders is OrderOrigin, OrderDestination {
155+
constructor(address _permit2) UsesPermit2(_permit2) {}
156+
}

0 commit comments

Comments
 (0)