diff --git a/.gas-snapshot b/.gas-snapshot index 1a8998c..60969ec 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -6,11 +6,11 @@ OrdersTest:test_fill_underflowETH() (gas: 115281) OrdersTest:test_initiate_ERC20() (gas: 81435) OrdersTest:test_initiate_ETH() (gas: 44949) OrdersTest:test_initiate_both() (gas: 118677) -OrdersTest:test_initiate_multiERC20() (gas: 688417) +OrdersTest:test_initiate_multiERC20() (gas: 722408) OrdersTest:test_initiate_multiETH() (gas: 75304) OrdersTest:test_onlyBuilder() (gas: 12815) OrdersTest:test_orderExpired() (gas: 27956) -OrdersTest:test_sweepERC20() (gas: 60402) +OrdersTest:test_sweepERC20() (gas: 60446) OrdersTest:test_sweepETH() (gas: 81940) OrdersTest:test_underflowETH() (gas: 63528) PassageTest:test_configureEnter() (gas: 82311) @@ -26,7 +26,11 @@ PassageTest:test_receive() (gas: 21384) PassageTest:test_setUp() (gas: 16968) PassageTest:test_transact() (gas: 58562) PassageTest:test_transact_defaultChain() (gas: 57475) -PassageTest:test_withdraw() (gas: 59033) +PassageTest:test_withdraw() (gas: 59166) +RollupPassageTest:test_exit() (gas: 22347) +RollupPassageTest:test_exitToken() (gas: 50183) +RollupPassageTest:test_fallback() (gas: 19883) +RollupPassageTest:test_receive() (gas: 19844) ZenithTest:test_addSequencer() (gas: 88121) ZenithTest:test_badSignature() (gas: 37241) ZenithTest:test_incorrectHostBlock() (gas: 35086) diff --git a/script/Zenith.s.sol b/script/Zenith.s.sol index 215edc0..4322cb7 100644 --- a/script/Zenith.s.sol +++ b/script/Zenith.s.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import {Script} from "forge-std/Script.sol"; import {Zenith} from "../src/Zenith.sol"; -import {Passage} from "../src/Passage.sol"; +import {Passage, RollupPassage} from "../src/Passage.sol"; import {HostOrders, RollupOrders} from "../src/Orders.sol"; contract ZenithScript is Script { @@ -23,8 +23,9 @@ contract ZenithScript is Script { // deploy: // forge script ZenithScript --sig "deployL2()" --rpc-url $L2_RPC_URL --private-key $PRIVATE_KEY --broadcast - function deployL2() public returns (RollupOrders m) { + function deployL2() public returns (RollupPassage p, RollupOrders m) { vm.startBroadcast(); + p = new RollupPassage(); m = new RollupOrders(); } diff --git a/src/Passage.sol b/src/Passage.sol index 8dd3c85..6b9eb15 100644 --- a/src/Passage.sol +++ b/src/Passage.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.24; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; -/// @notice A contract deployed to Host chain that allows tokens to enter the rollup, -/// and enables Builders to fulfill requests to exchange tokens on the Rollup for tokens on the Host. +/// @notice A contract deployed to Host chain that allows tokens to enter the rollup. contract Passage { /// @notice The chainId of rollup that Ether will be sent to by default when entering the rollup via fallback() or receive(). uint256 public immutable defaultRollupChainId; @@ -178,3 +178,46 @@ contract Passage { emit EnterConfigured(token, _canEnter); } } + +/// @notice Enables tokens to Exit the rollup. +contract RollupPassage { + /// @notice Emitted when native Ether exits the rollup. + /// @param hostRecipient - The *requested* recipient of tokens on the host chain. + /// @param amount - The amount of Ether exiting the rollup. + event Exit(address indexed hostRecipient, uint256 amount); + + /// @notice Emitted when ERC20 tokens exit the rollup. + /// @param hostRecipient - The *requested* recipient of tokens on the host chain. + /// @param token - The token exiting the rollup. + /// @param amount - The amount of ERC20s exiting the rollup. + event ExitToken(address indexed hostRecipient, address indexed token, uint256 amount); + + /// @notice Allows native Ether to exit the rollup by being sent directly to the contract. + fallback() external payable { + exit(msg.sender); + } + + /// @notice Allows native Ether to exit the rollup by being sent directly to the contract. + receive() external payable { + exit(msg.sender); + } + + /// @notice Allows native Ether to exit the rollup. + /// @param hostRecipient - The *requested* recipient of tokens on the host chain. + /// @custom:emits Exit indicating the amount of Ether that was locked on the rollup & the requested host recipient. + function exit(address hostRecipient) public payable { + if (msg.value == 0) return; + emit Exit(hostRecipient, msg.value); + } + + /// @notice Allows ERC20 tokens to exit the rollup. + /// @param hostRecipient - The *requested* recipient of tokens on the host chain. + /// @param token - The rollup address of the token exiting the rollup. + /// @param amount - The amount of tokens exiting the rollup. + function exitToken(address hostRecipient, address token, uint256 amount) public { + if (amount == 0) return; + IERC20(token).transferFrom(msg.sender, address(this), amount); + ERC20Burnable(token).burn(amount); + emit ExitToken(hostRecipient, token, amount); + } +} diff --git a/test/Helpers.t.sol b/test/Helpers.t.sol index 48e78be..c040458 100644 --- a/test/Helpers.t.sol +++ b/test/Helpers.t.sol @@ -4,8 +4,9 @@ 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"; +import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; -contract TestERC20 is ERC20 { +contract TestERC20 is ERC20Burnable { constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} function mint(address recipient, uint256 amount) external { diff --git a/test/Passage.t.sol b/test/Passage.t.sol index 34bb72e..82ee802 100644 --- a/test/Passage.t.sol +++ b/test/Passage.t.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.24; import {Test, console2} from "forge-std/Test.sol"; -import {Passage} from "../src/Passage.sol"; +import {Passage, RollupPassage} from "../src/Passage.sol"; import {TestERC20} from "./Helpers.t.sol"; import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {ERC20Burnable} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol"; contract PassageTest is Test { Passage public target; @@ -189,3 +190,52 @@ contract PassageTest is Test { target.withdraw(token, recipient, amount); } } + +contract RollupPassageTest is Test { + RollupPassage public target; + address token; + address recipient = address(0x123); + uint256 amount = 200; + + event Exit(address indexed hostRecipient, uint256 amount); + + event ExitToken(address indexed hostRecipient, address indexed token, uint256 amount); + + function setUp() public { + // deploy target + target = new RollupPassage(); + + // deploy token + token = address(new TestERC20("hi", "HI")); + TestERC20(token).mint(address(this), amount * 10000); + TestERC20(token).approve(address(target), amount * 10000); + } + + function test_receive() public { + vm.expectEmit(); + emit Exit(address(this), amount); + address(target).call{value: amount}(""); + } + + function test_fallback() public { + vm.expectEmit(); + emit Exit(address(this), amount); + address(target).call{value: amount}("0xabcd"); + } + + function test_exit() public { + vm.expectEmit(); + emit Exit(recipient, amount); + target.exit{value: amount}(recipient); + } + + function test_exitToken() public { + vm.expectEmit(); + emit ExitToken(recipient, token, amount); + vm.expectCall( + token, abi.encodeWithSelector(ERC20.transferFrom.selector, address(this), address(target), amount) + ); + vm.expectCall(token, abi.encodeWithSelector(ERC20Burnable.burn.selector, amount)); + target.exitToken(recipient, token, amount); + } +}