Skip to content

test: Orders #54

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

Merged
merged 8 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 10 additions & 0 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
OrdersTest:test_initiate_ERC20() (gas: 81255)
OrdersTest:test_initiate_ETH() (gas: 44801)
OrdersTest:test_initiate_both() (gas: 118573)
OrdersTest:test_initiate_multiERC20() (gas: 688314)
OrdersTest:test_initiate_multiETH() (gas: 75288)
OrdersTest:test_initiate_underflowETH() (gas: 63455)
OrdersTest:test_onlyBuilder() (gas: 12793)
OrdersTest:test_orderExpired() (gas: 27993)
OrdersTest:test_sweep_ERC20() (gas: 60250)
OrdersTest:test_sweep_ETH() (gas: 81788)
PassageTest:test_configureEnter() (gas: 82311)
PassageTest:test_disallowedEnter() (gas: 17916)
PassageTest:test_enter() (gas: 25563)
Expand Down
50 changes: 25 additions & 25 deletions src/Orders.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@ 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 The destination chain for this output
uint32 chainId;
}

/// @notice Contract capable of processing fulfillment of intent-based Orders.
abstract contract OrderDestination {
/// @notice Emitted when an Order's Output is sent to the recipient.
Expand Down Expand Up @@ -34,29 +57,6 @@ abstract contract OrderDestination {

/// @notice Contract capable of registering initiation of intent-based Orders.
abstract contract OrderOrigin {
/// @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 The destination chain for this output
uint32 chainId;
}

/// @notice Thrown when an Order is submitted with a deadline that has passed.
error OrderExpired();

Expand All @@ -79,14 +79,14 @@ abstract contract OrderOrigin {
/// @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.
/// @param deadline - The deadline by which the Order must be fulfilled.
/// @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.
/// @custom:reverts OrderExpired if the deadline has passed.
/// @custom:emits Order if the transaction mines.
function initiate(uint256 deadline, Input[] memory inputs, Output[] memory outputs) external payable {
// check that the deadline hasn't passed
if (block.timestamp >= deadline) revert OrderExpired();
if (block.timestamp > deadline) revert OrderExpired();

// transfer inputs to this contract
_transferInputs(inputs);
Expand Down
9 changes: 9 additions & 0 deletions test/Helpers.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ pragma solidity ^0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {Zenith} from "../src/Zenith.sol";
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

contract TestERC20 is ERC20 {
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}

function mint(address recipient, uint256 amount) external {
_mint(recipient, amount);
}
}

contract HelpersTest is Test {
Zenith public target;
Expand Down
179 changes: 179 additions & 0 deletions test/Orders.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {RollupOrders, Input, Output, OrderOrigin} from "../src/Orders.sol";
import {TestERC20} from "./Helpers.t.sol";
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

contract OrdersTest is Test {
RollupOrders public target;
Input[] public inputs;
Output[] public outputs;

mapping(address => bool) isToken;

address token;
uint32 chainId = 3;
address recipient = address(0x123);
uint256 amount = 200;
uint256 deadline = block.timestamp;

event OutputFilled(uint256 indexed originChainId, address indexed recipient, address indexed token, uint256 amount);

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

event Sweep(address indexed recipient, address indexed token, uint256 amount);

function setUp() public {
target = new RollupOrders();

// setup token
token = address(new TestERC20("hi", "HI"));
TestERC20(token).mint(address(this), amount * 10000);
TestERC20(token).approve(address(target), amount * 10000);
isToken[token] = true;

// setup Order Inputs/Outputs
Input memory input = Input(token, amount);
inputs.push(input);

Output memory output = Output(token, amount, recipient, chainId);
outputs.push(output);
}

// input ERC20
function test_initiate_ERC20() public {
// expect Order event is initiated, ERC20 is transferred
vm.expectEmit();
emit Order(deadline, inputs, outputs);
vm.expectCall(
token, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), address(target), amount)
);
target.initiate(deadline, inputs, outputs);
}

// input ETH
function test_initiate_ETH() public {
// change input to ETH
inputs[0].token = address(0);

// expect Order event is initiated
vm.expectEmit();
emit Order(deadline, inputs, outputs);
target.initiate{value: amount}(deadline, inputs, outputs);

// ETH is held in target contract
assertEq(address(target).balance, amount);
}

// input ETH and ERC20
function test_initiate_both() public {
// add ETH input
inputs.push(Input(address(0), amount));

// expect Order event is initiated, ERC20 is transferred
vm.expectEmit();
emit Order(deadline, inputs, outputs);
vm.expectCall(
token, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), address(target), amount)
);
target.initiate{value: amount}(deadline, inputs, outputs);

// ETH is held in target contract
assertEq(address(target).balance, amount);
}

// input multiple ERC20s
function test_initiate_multiERC20() public {
// setup second token
address token2 = address(new TestERC20("bye", "BYE"));
TestERC20(token2).mint(address(this), amount * 10000);
TestERC20(token2).approve(address(target), amount * 10000);

// add second token input
inputs.push(Input(token2, amount * 2));

// expect Order event is initiated, ERC20 is transferred
vm.expectEmit();
emit Order(deadline, inputs, outputs);
vm.expectCall(
token, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), address(target), amount)
);
vm.expectCall(
token2, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), address(target), amount * 2)
);
target.initiate(deadline, inputs, outputs);
}

// input multiple ETH inputs
function test_initiate_multiETH() public {
// change first input to ETH
inputs[0].token = address(0);
// add second ETH input
inputs.push(Input(address(0), amount * 2));

// expect Order event is initiated
vm.expectEmit();
emit Order(deadline, inputs, outputs);
target.initiate{value: amount * 3}(deadline, inputs, outputs);

// ETH is held in target contract
assertEq(address(target).balance, amount * 3);
}

function test_initiate_underflowETH() public {
// change first input to ETH
inputs[0].token = address(0);
// add second ETH input
inputs.push(Input(address(0), 1));

// total ETH inputs should be `amount` + 1; function should underflow only sending `amount`
vm.expectRevert();
target.initiate{value: amount}(deadline, inputs, outputs);
}

function test_orderExpired() public {
vm.warp(block.timestamp + 1);

vm.expectRevert(OrderOrigin.OrderExpired.selector);
target.initiate(deadline, inputs, outputs);
}

function test_sweep_ETH() public {
// set self as Builder
vm.coinbase(address(this));

// initiate an ETH order
inputs[0].token = address(0);
target.initiate{value: amount}(deadline, inputs, outputs);

assertEq(address(target).balance, amount);

// sweep ETH
vm.expectEmit();
emit Sweep(recipient, address(0), amount);
target.sweep(recipient, address(0));

assertEq(recipient.balance, amount);
}

function test_sweep_ERC20() public {
// set self as Builder
vm.coinbase(address(this));

// send ERC20 to the contract
TestERC20(token).transfer(address(target), amount);

// sweep ERC20
vm.expectEmit();
emit Sweep(recipient, token, amount);
vm.expectCall(token, abi.encodeWithSelector(ERC20.transfer.selector, recipient, amount));
target.sweep(recipient, token);
}

function test_onlyBuilder() public {
vm.expectRevert(OrderOrigin.OnlyBuilder.selector);
target.sweep(recipient, token);
}
}
9 changes: 1 addition & 8 deletions test/Passage.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,9 @@ pragma solidity ^0.8.24;

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

contract TestERC20 is ERC20 {
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}

function mint(address recipient, uint256 amount) external {
_mint(recipient, amount);
}
}

contract PassageTest is Test {
Passage public target;
address token;
Expand Down