Skip to content

Commit 5f26849

Browse files
committed
feat: update fill logic
1 parent 8d985cc commit 5f26849

File tree

2 files changed

+121
-40
lines changed

2 files changed

+121
-40
lines changed

src/Orders.sol

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,37 @@ struct Output {
2222
uint256 amount;
2323
/// @dev The address to receive the output tokens
2424
address recipient;
25-
/// @dev The destination chain for this output
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.
2627
uint32 chainId;
2728
}
2829

2930
/// @notice Contract capable of processing fulfillment of intent-based Orders.
3031
abstract contract OrderDestination {
31-
/// @notice Emitted when an Order's Output is sent to the recipient.
32-
/// @dev There may be multiple Outputs per Order.
33-
/// @param originChainId - The chainId on which the Order was initiated.
34-
/// @param recipient - The recipient of the token.
35-
/// @param token - The address of the token transferred to the recipient. address(0) corresponds to native Ether.
36-
/// @param amount - The amount of the token transferred to the recipient.
37-
event OutputFilled(uint256 indexed originChainId, address indexed recipient, address indexed token, uint256 amount);
38-
39-
/// @notice Send the Output(s) of an Order to fulfill it.
40-
/// The user calls `initiate` on a rollup; the Builder calls `fill` on the target chain for each Output.
41-
/// @custom:emits OutputFilled
42-
/// @param originChainId - The chainId on which the Order was initiated.
43-
/// @param recipient - The recipient of the token.
44-
/// @param token - The address of the token to be transferred to the recipient.
45-
/// address(0) corresponds to native Ether.
46-
/// @param amount - The amount of the token to be transferred to the recipient.
47-
function fill(uint256 originChainId, address recipient, address token, uint256 amount) external payable {
48-
if (token == address(0)) {
49-
require(amount == msg.value);
50-
payable(recipient).transfer(msg.value);
51-
} else {
52-
IERC20(token).transferFrom(msg.sender, recipient, amount);
32+
/// @notice Emitted when Order Outputs are sent to their recipients.
33+
/// @param outputs - The Outputs transferred to their recipients
34+
/// @dev NOTE that here, Output.chainId denotes the *origin* chainId.
35+
event Filled(Output[] outputs);
36+
37+
/// @notice Send the Output(s) of any number of Orders.
38+
/// The user calls `initiate` on a rollup; the Builder calls `fill` on the target chain aggregating Outputs.
39+
/// @param outputs - The Outputs to be transferred.
40+
/// @dev NOTE that here, Output.chainId denotes the *origin* chainId.
41+
/// @custom:emits Filled
42+
function fill(Output[] memory outputs) external payable {
43+
// transfer outputs
44+
uint256 value = msg.value;
45+
for (uint256 i; i < outputs.length; i++) {
46+
if (outputs[i].token == address(0)) {
47+
// this line should underflow if there's an attempt to spend more ETH than is attached to the transaction
48+
value -= outputs[i].amount;
49+
payable(outputs[i].recipient).transfer(outputs[i].amount);
50+
} else {
51+
IERC20(outputs[i].token).transferFrom(msg.sender, outputs[i].recipient, outputs[i].amount);
52+
}
5353
}
54-
emit OutputFilled(originChainId, recipient, token, amount);
54+
// emit
55+
emit Filled(outputs);
5556
}
5657
}
5758

@@ -64,6 +65,7 @@ abstract contract OrderOrigin {
6465
error OnlyBuilder();
6566

6667
/// @notice Emitted when an Order is submitted for fulfillment.
68+
/// @dev NOTE that here, Output.chainId denotes the *destination* chainId.
6769
event Order(uint256 deadline, Input[] inputs, Output[] outputs);
6870

6971
/// @notice Emitted when tokens or native Ether is swept from the contract.

test/Orders.t.sol

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ contract OrdersTest is Test {
1919
uint256 amount = 200;
2020
uint256 deadline = block.timestamp;
2121

22-
event OutputFilled(uint256 indexed originChainId, address indexed recipient, address indexed token, uint256 amount);
22+
event Filled(Output[] outputs);
2323

2424
event Order(uint256 deadline, Input[] inputs, Output[] outputs);
2525

@@ -176,6 +176,65 @@ contract OrdersTest is Test {
176176
vm.expectRevert(OrderOrigin.OnlyBuilder.selector);
177177
target.sweep(recipient, token);
178178
}
179+
180+
function test_fill_ETH() public {
181+
outputs[0].token = address(0);
182+
183+
vm.expectEmit();
184+
emit Filled(outputs);
185+
target.fill{value: amount}(outputs);
186+
187+
// ETH is transferred to recipient
188+
assertEq(recipient.balance, amount);
189+
}
190+
191+
function test_fill_ERC20() public {
192+
vm.expectEmit();
193+
emit Filled(outputs);
194+
vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), recipient, amount));
195+
target.fill(outputs);
196+
}
197+
198+
function test_fill_both() public {
199+
// add ETH output
200+
outputs.push(Output(address(0), amount * 2, recipient, chainId));
201+
202+
// expect Outputs are filled, ERC20 is transferred
203+
vm.expectEmit();
204+
emit Filled(outputs);
205+
vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), recipient, amount));
206+
target.fill{value: amount * 2}(outputs);
207+
208+
// ETH is transferred to recipient
209+
assertEq(recipient.balance, amount * 2);
210+
}
211+
212+
// fill multiple ETH outputs
213+
function test_fill_multiETH() public {
214+
// change first output to ETH
215+
outputs[0].token = address(0);
216+
// add second ETH oputput
217+
outputs.push(Output(address(0), amount * 2, recipient, chainId));
218+
219+
// expect Order event is initiated
220+
vm.expectEmit();
221+
emit Filled(outputs);
222+
target.fill{value: amount * 3}(outputs);
223+
224+
// ETH is transferred to recipient
225+
assertEq(recipient.balance, amount * 3);
226+
}
227+
228+
function test_fill_underflowETH() public {
229+
// change first output to ETH
230+
outputs[0].token = address(0);
231+
// add second ETH output
232+
outputs.push(Output(address(0), 1, recipient, chainId));
233+
234+
// total ETH outputs should be `amount` + 1; function should underflow only sending `amount`
235+
vm.expectRevert();
236+
target.fill{value: amount}(outputs);
237+
}
179238
}
180239

181240
contract OrdersFuzzTest is Test {
@@ -184,7 +243,7 @@ contract OrdersFuzzTest is Test {
184243

185244
uint256 deadline = block.timestamp + 100;
186245

187-
event OutputFilled(uint256 indexed originChainId, address indexed recipient, address indexed token, uint256 amount);
246+
event Filled(Output[] outputs);
188247

189248
event Order(uint256 deadline, Input[] inputs, Output[] outputs);
190249

@@ -202,7 +261,7 @@ contract OrdersFuzzTest is Test {
202261

203262
function testFuzz_initiate(Input[] memory fuzzInputs, Output[] memory fuzzOutputs) public {
204263
// can only use vm.assume 256 * 256 times, and we do 256 fuzz runs
205-
vm.assume(fuzzInputs.length < 256);
264+
vm.assume(fuzzInputs.length < 250);
206265
// setup the fuzz test by ensuring ETH and ERC20 amounts are available
207266
uint256 totalETH;
208267
for (uint256 i; i < fuzzInputs.length; i++) {
@@ -212,19 +271,7 @@ contract OrdersFuzzTest is Test {
212271
continue;
213272
}
214273

215-
// ERC20 inputs must be one of the tokens we've deployed
216-
vm.assume(isToken[fuzzInputs[i].token]);
217-
// mint the token amount to ensure the address has it
218-
TestERC20(fuzzInputs[i].token).mint(address(this), fuzzInputs[i].amount);
219-
// approve the target to spend the total token amount
220-
TestERC20(fuzzInputs[i].token).approve(address(target), type(uint256).max);
221-
// expect the token amount to be transferred
222-
vm.expectCall(
223-
fuzzInputs[i].token,
224-
abi.encodeWithSelector(
225-
ERC20.transferFrom.selector, address(this), address(target), fuzzInputs[i].amount
226-
)
227-
);
274+
_setupToken(fuzzInputs[i].token, fuzzInputs[i].amount, address(target));
228275
}
229276
// deal the total ETH amount to ensure the address has it
230277
vm.deal(address(this), address(this).balance + totalETH);
@@ -233,4 +280,36 @@ contract OrdersFuzzTest is Test {
233280
emit Order(deadline, fuzzInputs, fuzzOutputs);
234281
target.initiate{value: totalETH}(deadline, fuzzInputs, fuzzOutputs);
235282
}
283+
284+
function testFuzz_fill(Output[] memory fuzzOutputs) public {
285+
// can only use vm.assume 256 * 256 times, and we do 256 fuzz runs
286+
vm.assume(fuzzOutputs.length < 250);
287+
// setup the fuzz test by ensuring ETH and ERC20 amounts are available
288+
uint256 totalETH;
289+
for (uint256 i; i < fuzzOutputs.length; i++) {
290+
if (fuzzOutputs[i].token == address(0)) {
291+
totalETH += fuzzOutputs[i].amount;
292+
continue;
293+
}
294+
295+
_setupToken(fuzzOutputs[i].token, fuzzOutputs[i].amount, fuzzOutputs[i].recipient);
296+
}
297+
// deal the total ETH amount to ensure the address has it
298+
vm.deal(address(this), address(this).balance + totalETH);
299+
300+
vm.expectEmit();
301+
emit Filled(fuzzOutputs);
302+
target.fill{value: totalETH}(fuzzOutputs);
303+
}
304+
305+
function _setupToken(address token, uint256 amount, address recipient) internal {
306+
// ERC20 inputs must be one of the tokens we've deployed
307+
vm.assume(isToken[token]);
308+
// mint the token amount to ensure the address has it
309+
TestERC20(token).mint(address(this), amount);
310+
// approve the target to spend the total token amount
311+
TestERC20(token).approve(address(target), type(uint256).max);
312+
// expect the token amount to be transferred
313+
vm.expectCall(token, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), recipient, amount));
314+
}
236315
}

0 commit comments

Comments
 (0)