Skip to content

Commit 3cbe2fe

Browse files
feat: permit2 for token flows (#63)
* 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]>
1 parent 9e8e460 commit 3cbe2fe

File tree

17 files changed

+986
-130
lines changed

17 files changed

+986
-130
lines changed

.gas-snapshot

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,45 @@
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)
18-
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)
1+
OrderOriginPermit2Test:test_fillPermit2() (gas: 225289)
2+
OrderOriginPermit2Test:test_fillPermit2_multi() (gas: 1019134)
3+
OrderOriginPermit2Test:test_initiatePermit2() (gas: 235752)
4+
OrderOriginPermit2Test:test_initiatePermit2_multi() (gas: 989274)
5+
OrdersTest:test_fill_ERC20() (gas: 70537)
6+
OrdersTest:test_fill_ETH() (gas: 68498)
7+
OrdersTest:test_fill_both() (gas: 166773)
8+
OrdersTest:test_fill_multiETH() (gas: 132119)
9+
OrdersTest:test_fill_underflowETH() (gas: 115403)
10+
OrdersTest:test_initiate_ERC20() (gas: 81636)
11+
OrdersTest:test_initiate_ETH() (gas: 45150)
12+
OrdersTest:test_initiate_both() (gas: 118911)
13+
OrdersTest:test_initiate_multiERC20() (gas: 722642)
14+
OrdersTest:test_initiate_multiETH() (gas: 75538)
15+
OrdersTest:test_orderExpired() (gas: 28106)
16+
OrdersTest:test_sweepERC20() (gas: 60491)
17+
OrdersTest:test_sweepETH() (gas: 82186)
18+
OrdersTest:test_underflowETH() (gas: 63690)
19+
PassagePermit2Test:test_disallowedEnterPermit2() (gas: 699630)
20+
PassagePermit2Test:test_enterTokenPermit2() (gas: 145449)
21+
PassageTest:test_configureEnter() (gas: 125771)
22+
PassageTest:test_disallowedEnter() (gas: 56619)
23+
PassageTest:test_enter() (gas: 25519)
24+
PassageTest:test_enterToken() (gas: 64397)
25+
PassageTest:test_enterToken_defaultChain() (gas: 62979)
26+
PassageTest:test_enter_defaultChain() (gas: 24055)
27+
PassageTest:test_fallback() (gas: 21533)
2328
PassageTest:test_onlyTokenAdmin() (gas: 16881)
24-
PassageTest:test_receive() (gas: 21339)
25-
PassageTest:test_setUp() (gas: 16901)
29+
PassageTest:test_receive() (gas: 21383)
30+
PassageTest:test_setUp() (gas: 17011)
2631
PassageTest:test_withdraw() (gas: 59188)
27-
RollupPassageTest:test_exit() (gas: 22347)
28-
RollupPassageTest:test_exitToken() (gas: 50183)
29-
RollupPassageTest:test_fallback() (gas: 19883)
32+
RollupPassagePermit2Test:test_exitTokenPermit2() (gas: 129402)
33+
RollupPassageTest:test_exit() (gas: 22403)
34+
RollupPassageTest:test_exitToken() (gas: 50232)
35+
RollupPassageTest:test_fallback() (gas: 19949)
3036
RollupPassageTest:test_receive() (gas: 19844)
3137
TransactTest:test_configureGas() (gas: 22828)
32-
TransactTest:test_enterTransact() (gas: 103961)
38+
TransactTest:test_enterTransact() (gas: 103973)
3339
TransactTest:test_onlyGasAdmin() (gas: 8810)
3440
TransactTest:test_setUp() (gas: 17494)
35-
TransactTest:test_transact() (gas: 101431)
36-
TransactTest:test_transact_defaultChain() (gas: 100544)
41+
TransactTest:test_transact() (gas: 101443)
42+
TransactTest:test_transact_defaultChain() (gas: 100556)
3743
TransactTest:test_transact_globalGasLimit() (gas: 105063)
3844
TransactTest:test_transact_perTransactGasLimit() (gas: 32774)
3945
ZenithTest:test_addSequencer() (gas: 88121)

.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 }}

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
[submodule "lib/openzeppelin-contracts"]
55
path = lib/openzeppelin-contracts
66
url = https://github.com/OpenZeppelin/openzeppelin-contracts
7+
[submodule "lib/permit2"]
8+
path = lib/permit2
9+
url = https://github.com/Uniswap/permit2

lib/permit2

Submodule permit2 added at cc56ad0

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/Orders.sol

Lines changed: 79 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,51 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma solidity ^0.8.24;
33

4+
import {OrdersPermit2, UsesPermit2} from "./permit2/UsesPermit2.sol";
5+
import {IOrders} from "./interfaces/IOrders.sol";
46
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
57

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-
}
29-
308
/// @notice Contract capable of processing fulfillment of intent-based Orders.
31-
abstract contract OrderDestination {
9+
abstract contract OrderDestination is IOrders, OrdersPermit2 {
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 Fill any number of Order(s), by transferring their Output(s) via permit2 signed batch transfer.
28+
/// @dev Can only provide ERC20 tokens as Outputs.
29+
/// @dev Filler may aggregate multiple Outputs with the same (`chainId`, `recipient`, `token`) into a single Output with the summed `amount`.
30+
/// @dev the permit2 signer is the Filler providing the Outputs.
31+
/// @dev the permit2 `permitted` tokens MUST match provided Outputs.
32+
/// @dev Filler MUST submit `fill` and `intitiate` within an atomic bundle.
33+
/// @dev NOTE that here, Output.chainId denotes the *origin* chainId.
34+
/// @param outputs - The Outputs to be transferred. signed over via permit2 witness.
35+
/// @param permit2 - the permit2 details, signer, and signature.
36+
/// @custom:emits Filled
37+
function fillPermit2(Output[] memory outputs, OrdersPermit2.Permit2Batch calldata permit2) external {
38+
// transfer all tokens to the Output recipients via permit2 (includes check on nonce & deadline)
39+
_permitWitnessTransferFrom(
40+
outputWitness(outputs), _fillTransferDetails(outputs, permit2.permit.permitted), permit2
41+
);
42+
43+
// emit
44+
emit Filled(outputs);
45+
}
46+
47+
/// @notice Transfer the Order Outputs to their recipients.
48+
function _transferOutputs(Output[] memory outputs) internal {
4449
uint256 value = msg.value;
4550
for (uint256 i; i < outputs.length; i++) {
4651
if (outputs[i].token == address(0)) {
@@ -51,19 +56,14 @@ abstract contract OrderDestination {
5156
IERC20(outputs[i].token).transferFrom(msg.sender, outputs[i].recipient, outputs[i].amount);
5257
}
5358
}
54-
// emit
55-
emit Filled(outputs);
5659
}
5760
}
5861

5962
/// @notice Contract capable of registering initiation of intent-based Orders.
60-
abstract contract OrderOrigin {
63+
abstract contract OrderOrigin is IOrders, OrdersPermit2 {
6164
/// @notice Thrown when an Order is submitted with a deadline that has passed.
6265
error OrderExpired();
6366

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

76-
/// @notice Request to swap ERC20s.
76+
/// @notice Initiate an Order.
77+
/// @dev Filler MUST submit `fill` and `intitiate` + `sweep` within an atomic bundle.
78+
/// @dev NOTE that here, Output.chainId denotes the *target* chainId.
7779
/// @dev inputs are provided on the rollup; in exchange,
7880
/// 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.
8281
/// @dev The Rollup STF MUST NOT apply `initiate` transactions to the rollup state
8382
/// UNLESS the outputs are delivered on the target chains within the same block.
83+
/// @dev Fees paid to the Builders for fulfilling the Orders
84+
/// can be included within the "exchange rate" between inputs and outputs.
8485
/// @param deadline - The deadline at or before which the Order must be fulfilled.
8586
/// @param inputs - The token amounts offered by the swapper in exchange for the outputs.
8687
/// @param outputs - The token amounts that must be received on their target chain(s) in order for the Order to be executed.
@@ -97,17 +98,26 @@ abstract contract OrderOrigin {
9798
emit Order(deadline, inputs, outputs);
9899
}
99100

100-
/// @notice Transfer the Order inputs to this contract, where they can be collected by the Order filler.
101-
function _transferInputs(Input[] memory inputs) internal {
102-
uint256 value = msg.value;
103-
for (uint256 i; i < inputs.length; i++) {
104-
if (inputs[i].token == address(0)) {
105-
// this line should underflow if there's an attempt to spend more ETH than is attached to the transaction
106-
value -= inputs[i].amount;
107-
} else {
108-
IERC20(inputs[i].token).transferFrom(msg.sender, address(this), inputs[i].amount);
109-
}
110-
}
101+
/// @notice Initiate an Order, transferring Input tokens to the Filler via permit2 signed batch transfer.
102+
/// @dev Can only provide ERC20 tokens as Inputs.
103+
/// @dev the permit2 signer is the swapper providing the Input tokens in exchange for the Outputs.
104+
/// @dev Filler MUST submit `fill` and `intitiate` within an atomic bundle.
105+
/// @dev NOTE that here, Output.chainId denotes the *target* chainId.
106+
/// @param tokenRecipient - the recipient of the Input tokens, provided by msg.sender (un-verified by permit2).
107+
/// @param outputs - the Outputs required in exchange for the Input tokens. signed over via permit2 witness.
108+
/// @param permit2 - the permit2 details, signer, and signature.
109+
function initiatePermit2(
110+
address tokenRecipient,
111+
Output[] memory outputs,
112+
OrdersPermit2.Permit2Batch calldata permit2
113+
) external {
114+
// transfer all tokens to the tokenRecipient via permit2 (includes check on nonce & deadline)
115+
_permitWitnessTransferFrom(
116+
outputWitness(outputs), _initiateTransferDetails(tokenRecipient, permit2.permit.permitted), permit2
117+
);
118+
119+
// emit
120+
emit Order(permit2.permit.deadline, _inputs(permit2.permit.permitted), outputs);
111121
}
112122

113123
/// @notice Transfer the entire balance of ERC20 tokens to the recipient.
@@ -118,8 +128,7 @@ abstract contract OrderOrigin {
118128
/// @param token - The token to transfer.
119129
/// @custom:emits Sweep
120130
/// @custom:reverts OnlyBuilder if called by non-block builder
121-
function sweep(address recipient, address token) public {
122-
if (msg.sender != block.coinbase) revert OnlyBuilder();
131+
function sweep(address recipient, address token) external {
123132
// send ETH or tokens
124133
uint256 balance;
125134
if (token == address(0)) {
@@ -131,8 +140,25 @@ abstract contract OrderOrigin {
131140
}
132141
emit Sweep(recipient, token, balance);
133142
}
143+
144+
/// @notice Transfer the Order inputs to this contract, where they can be collected by the Order filler via `sweep`.
145+
function _transferInputs(Input[] memory inputs) internal {
146+
uint256 value = msg.value;
147+
for (uint256 i; i < inputs.length; i++) {
148+
if (inputs[i].token == address(0)) {
149+
// this line should underflow if there's an attempt to spend more ETH than is attached to the transaction
150+
value -= inputs[i].amount;
151+
} else {
152+
IERC20(inputs[i].token).transferFrom(msg.sender, address(this), inputs[i].amount);
153+
}
154+
}
155+
}
134156
}
135157

136-
contract HostOrders is OrderDestination {}
158+
contract HostOrders is OrderDestination {
159+
constructor(address _permit2) UsesPermit2(_permit2) {}
160+
}
137161

138-
contract RollupOrders is OrderOrigin, OrderDestination {}
162+
contract RollupOrders is OrderOrigin, OrderDestination {
163+
constructor(address _permit2) UsesPermit2(_permit2) {}
164+
}

0 commit comments

Comments
 (0)