Skip to content

Commit 6fdfef6

Browse files
authored
test: Orders (#54)
* fix: allow submitting Order *at* deadline: * move structs to top of file * test: Orders * prevent vm.assume overflow * add snapshot * rename funcs * update snapshot * remove fuzz tests for now
1 parent 5e32da8 commit 6fdfef6

File tree

5 files changed

+224
-33
lines changed

5 files changed

+224
-33
lines changed

.gas-snapshot

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
OrdersTest:test_initiate_ERC20() (gas: 81255)
2+
OrdersTest:test_initiate_ETH() (gas: 44801)
3+
OrdersTest:test_initiate_both() (gas: 118573)
4+
OrdersTest:test_initiate_multiERC20() (gas: 688314)
5+
OrdersTest:test_initiate_multiETH() (gas: 75288)
6+
OrdersTest:test_initiate_underflowETH() (gas: 63455)
7+
OrdersTest:test_onlyBuilder() (gas: 12793)
8+
OrdersTest:test_orderExpired() (gas: 27993)
9+
OrdersTest:test_sweep_ERC20() (gas: 60250)
10+
OrdersTest:test_sweep_ETH() (gas: 81788)
111
PassageTest:test_configureEnter() (gas: 82311)
212
PassageTest:test_disallowedEnter() (gas: 17916)
313
PassageTest:test_enter() (gas: 25563)

src/Orders.sol

+25-25
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,29 @@ pragma solidity ^0.8.24;
33

44
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
55

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 The destination chain for this output
26+
uint32 chainId;
27+
}
28+
629
/// @notice Contract capable of processing fulfillment of intent-based Orders.
730
abstract contract OrderDestination {
831
/// @notice Emitted when an Order's Output is sent to the recipient.
@@ -34,29 +57,6 @@ abstract contract OrderDestination {
3457

3558
/// @notice Contract capable of registering initiation of intent-based Orders.
3659
abstract contract OrderOrigin {
37-
/// @notice Tokens sent by the swapper as inputs to the order
38-
/// @dev From ERC-7683
39-
struct Input {
40-
/// @dev The address of the ERC20 token on the origin chain
41-
address token;
42-
/// @dev The amount of the token to be sent
43-
uint256 amount;
44-
}
45-
46-
/// @notice Tokens that must be receive for a valid order fulfillment
47-
/// @dev From ERC-7683
48-
struct Output {
49-
/// @dev The address of the ERC20 token on the destination chain
50-
/// @dev address(0) used as a sentinel for the native token
51-
address token;
52-
/// @dev The amount of the token to be sent
53-
uint256 amount;
54-
/// @dev The address to receive the output tokens
55-
address recipient;
56-
/// @dev The destination chain for this output
57-
uint32 chainId;
58-
}
59-
6060
/// @notice Thrown when an Order is submitted with a deadline that has passed.
6161
error OrderExpired();
6262

@@ -79,14 +79,14 @@ abstract contract OrderOrigin {
7979
/// @dev The Builder claims the inputs from the contract by submitting `sweep` transactions within the same block.
8080
/// @dev The Rollup STF MUST NOT apply `initiate` transactions to the rollup state
8181
/// UNLESS the outputs are delivered on the target chains within the same block.
82-
/// @param deadline - The deadline by which the Order must be fulfilled.
82+
/// @param deadline - The deadline at or before which the Order must be fulfilled.
8383
/// @param inputs - The token amounts offered by the swapper in exchange for the outputs.
8484
/// @param outputs - The token amounts that must be received on their target chain(s) in order for the Order to be executed.
8585
/// @custom:reverts OrderExpired if the deadline has passed.
8686
/// @custom:emits Order if the transaction mines.
8787
function initiate(uint256 deadline, Input[] memory inputs, Output[] memory outputs) external payable {
8888
// check that the deadline hasn't passed
89-
if (block.timestamp >= deadline) revert OrderExpired();
89+
if (block.timestamp > deadline) revert OrderExpired();
9090

9191
// transfer inputs to this contract
9292
_transferInputs(inputs);

test/Helpers.t.sol

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ pragma solidity ^0.8.24;
33

44
import {Test, console2} from "forge-std/Test.sol";
55
import {Zenith} from "../src/Zenith.sol";
6+
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
7+
8+
contract TestERC20 is ERC20 {
9+
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}
10+
11+
function mint(address recipient, uint256 amount) external {
12+
_mint(recipient, amount);
13+
}
14+
}
615

716
contract HelpersTest is Test {
817
Zenith public target;

test/Orders.t.sol

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.24;
3+
4+
import {Test, console2} from "forge-std/Test.sol";
5+
import {RollupOrders, Input, Output, OrderOrigin} from "../src/Orders.sol";
6+
import {TestERC20} from "./Helpers.t.sol";
7+
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
8+
9+
contract OrdersTest is Test {
10+
RollupOrders public target;
11+
Input[] public inputs;
12+
Output[] public outputs;
13+
14+
mapping(address => bool) isToken;
15+
16+
address token;
17+
uint32 chainId = 3;
18+
address recipient = address(0x123);
19+
uint256 amount = 200;
20+
uint256 deadline = block.timestamp;
21+
22+
event OutputFilled(uint256 indexed originChainId, address indexed recipient, address indexed token, uint256 amount);
23+
24+
event Order(uint256 deadline, Input[] inputs, Output[] outputs);
25+
26+
event Sweep(address indexed recipient, address indexed token, uint256 amount);
27+
28+
function setUp() public {
29+
target = new RollupOrders();
30+
31+
// setup token
32+
token = address(new TestERC20("hi", "HI"));
33+
TestERC20(token).mint(address(this), amount * 10000);
34+
TestERC20(token).approve(address(target), amount * 10000);
35+
isToken[token] = true;
36+
37+
// setup Order Inputs/Outputs
38+
Input memory input = Input(token, amount);
39+
inputs.push(input);
40+
41+
Output memory output = Output(token, amount, recipient, chainId);
42+
outputs.push(output);
43+
}
44+
45+
// input ERC20
46+
function test_initiate_ERC20() public {
47+
// expect Order event is initiated, ERC20 is transferred
48+
vm.expectEmit();
49+
emit Order(deadline, inputs, outputs);
50+
vm.expectCall(
51+
token, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), address(target), amount)
52+
);
53+
target.initiate(deadline, inputs, outputs);
54+
}
55+
56+
// input ETH
57+
function test_initiate_ETH() public {
58+
// change input to ETH
59+
inputs[0].token = address(0);
60+
61+
// expect Order event is initiated
62+
vm.expectEmit();
63+
emit Order(deadline, inputs, outputs);
64+
target.initiate{value: amount}(deadline, inputs, outputs);
65+
66+
// ETH is held in target contract
67+
assertEq(address(target).balance, amount);
68+
}
69+
70+
// input ETH and ERC20
71+
function test_initiate_both() public {
72+
// add ETH input
73+
inputs.push(Input(address(0), amount));
74+
75+
// expect Order event is initiated, ERC20 is transferred
76+
vm.expectEmit();
77+
emit Order(deadline, inputs, outputs);
78+
vm.expectCall(
79+
token, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), address(target), amount)
80+
);
81+
target.initiate{value: amount}(deadline, inputs, outputs);
82+
83+
// ETH is held in target contract
84+
assertEq(address(target).balance, amount);
85+
}
86+
87+
// input multiple ERC20s
88+
function test_initiate_multiERC20() public {
89+
// setup second token
90+
address token2 = address(new TestERC20("bye", "BYE"));
91+
TestERC20(token2).mint(address(this), amount * 10000);
92+
TestERC20(token2).approve(address(target), amount * 10000);
93+
94+
// add second token input
95+
inputs.push(Input(token2, amount * 2));
96+
97+
// expect Order event is initiated, ERC20 is transferred
98+
vm.expectEmit();
99+
emit Order(deadline, inputs, outputs);
100+
vm.expectCall(
101+
token, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), address(target), amount)
102+
);
103+
vm.expectCall(
104+
token2, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), address(target), amount * 2)
105+
);
106+
target.initiate(deadline, inputs, outputs);
107+
}
108+
109+
// input multiple ETH inputs
110+
function test_initiate_multiETH() public {
111+
// change first input to ETH
112+
inputs[0].token = address(0);
113+
// add second ETH input
114+
inputs.push(Input(address(0), amount * 2));
115+
116+
// expect Order event is initiated
117+
vm.expectEmit();
118+
emit Order(deadline, inputs, outputs);
119+
target.initiate{value: amount * 3}(deadline, inputs, outputs);
120+
121+
// ETH is held in target contract
122+
assertEq(address(target).balance, amount * 3);
123+
}
124+
125+
function test_initiate_underflowETH() public {
126+
// change first input to ETH
127+
inputs[0].token = address(0);
128+
// add second ETH input
129+
inputs.push(Input(address(0), 1));
130+
131+
// total ETH inputs should be `amount` + 1; function should underflow only sending `amount`
132+
vm.expectRevert();
133+
target.initiate{value: amount}(deadline, inputs, outputs);
134+
}
135+
136+
function test_orderExpired() public {
137+
vm.warp(block.timestamp + 1);
138+
139+
vm.expectRevert(OrderOrigin.OrderExpired.selector);
140+
target.initiate(deadline, inputs, outputs);
141+
}
142+
143+
function test_sweep_ETH() public {
144+
// set self as Builder
145+
vm.coinbase(address(this));
146+
147+
// initiate an ETH order
148+
inputs[0].token = address(0);
149+
target.initiate{value: amount}(deadline, inputs, outputs);
150+
151+
assertEq(address(target).balance, amount);
152+
153+
// sweep ETH
154+
vm.expectEmit();
155+
emit Sweep(recipient, address(0), amount);
156+
target.sweep(recipient, address(0));
157+
158+
assertEq(recipient.balance, amount);
159+
}
160+
161+
function test_sweep_ERC20() public {
162+
// set self as Builder
163+
vm.coinbase(address(this));
164+
165+
// send ERC20 to the contract
166+
TestERC20(token).transfer(address(target), amount);
167+
168+
// sweep ERC20
169+
vm.expectEmit();
170+
emit Sweep(recipient, token, amount);
171+
vm.expectCall(token, abi.encodeWithSelector(ERC20.transfer.selector, recipient, amount));
172+
target.sweep(recipient, token);
173+
}
174+
175+
function test_onlyBuilder() public {
176+
vm.expectRevert(OrderOrigin.OnlyBuilder.selector);
177+
target.sweep(recipient, token);
178+
}
179+
}

test/Passage.t.sol

+1-8
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,9 @@ pragma solidity ^0.8.24;
33

44
import {Test, console2} from "forge-std/Test.sol";
55
import {Passage} from "../src/Passage.sol";
6+
import {TestERC20} from "./Helpers.t.sol";
67
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
78

8-
contract TestERC20 is ERC20 {
9-
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}
10-
11-
function mint(address recipient, uint256 amount) external {
12-
_mint(recipient, amount);
13-
}
14-
}
15-
169
contract PassageTest is Test {
1710
Passage public target;
1811
address token;

0 commit comments

Comments
 (0)