From 261b2e6195203fa0b809829e2c15c130d1477501 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 2 Dec 2025 08:06:37 -0500 Subject: [PATCH 1/5] feat: SignetL1 --- foundry.toml | 4 +- src/apps/Morpho.sol | 2 + src/chains/Pecorino.sol | 8 ++ src/l1/Signet.sol | 127 ++++++++++++++++++++++++++++++ src/l2/Signet.sol | 2 +- src/vendor/AddressAliasHelper.sol | 43 ++++++++++ 6 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 src/l1/Signet.sol create mode 100644 src/vendor/AddressAliasHelper.sol diff --git a/foundry.toml b/foundry.toml index 7d4a7dc..4707845 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,8 +5,6 @@ libs = ["lib"] via-ir = true -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options - - [lint] +ignore = ["src/vendor/*"] exclude_lints = ["unwrapped-modifier-logic"] \ No newline at end of file diff --git a/src/apps/Morpho.sol b/src/apps/Morpho.sol index fb1e653..e557475 100644 --- a/src/apps/Morpho.sol +++ b/src/apps/Morpho.sol @@ -249,9 +249,11 @@ contract HosyMorphoBorrow is HostMorphoUser { // borrow some amount of loanToken MORPHO.borrow(loadParams(), amount, 0, onBehalf, address(this)); + // TODO: complete implementation // User logic to use the tokens goes here. // Could send the tokens to the rollup via Passage, or do something // else :) + filler; return true; } diff --git a/src/chains/Pecorino.sol b/src/chains/Pecorino.sol index 8658524..a760618 100644 --- a/src/chains/Pecorino.sol +++ b/src/chains/Pecorino.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.13; import {RollupOrders} from "zenith/src/orders/RollupOrders.sol"; import {RollupPassage} from "zenith/src/passage/RollupPassage.sol"; +import {HostOrders} from "zenith/src/orders/HostOrders.sol"; +import {Passage} from "zenith/src/passage/Passage.sol"; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; /// @title PecorinoConstants @@ -16,6 +18,12 @@ library PecorinoConstants { /// @notice The Pecorino Rollup chain ID. uint32 constant ROLLUP_CHAIN_ID = 14174; + /// @notice The Passage contract for the Pecorino testnet host chain. + Passage constant HOST_PASSAGE = Passage(payable(0x12585352AA1057443D6163B539EfD4487f023182)); + + /// @notice The HostOrders contract for the Pecorino testnet host chain. + HostOrders constant HOST_ORDERS = HostOrders(0x0A4f505364De0Aa46c66b15aBae44eBa12ab0380); + /// @notice The Rollup Passage contract for the Pecorino testnet. RollupPassage constant PECORINO_ROLLUP_PASSAGE = RollupPassage(payable(0x0000000000007369676E65742D70617373616765)); diff --git a/src/l1/Signet.sol b/src/l1/Signet.sol new file mode 100644 index 0000000..0dee21a --- /dev/null +++ b/src/l1/Signet.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {HostOrders} from "zenith/src/orders/HostOrders.sol"; +import {Passage} from "zenith/src/passage/Passage.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +import {AddressAliasHelper} from "../vendor/AddressAliasHelper.sol"; +import {PecorinoConstants} from "../chains/Pecorino.sol"; + +abstract contract SignetL1 { + /// @notice Sentinal value for the native asset in order inputs/outputs + address constant NATIVE_ASSET = address(0); + + /// @notice The Passage address + Passage internal immutable PASSAGE; + /// @notice The Host Orders address + HostOrders internal immutable ORDERS; + + /// @notice The WETH token address. + IERC20 internal immutable WETH; + /// @notice The WBTC token address. + IERC20 internal immutable WBTC; + /// @notice The USDC token address. + IERC20 internal immutable USDC; + /// @notice The USDT token address. + IERC20 internal immutable USDT; + + /// @notice The Rollup WUSD token address. + address internal immutable RU_WUSD; + /// @notice The Rollup WBTC token address. + address internal immutable RU_WBTC; + /// @notice The Rollup WETH token address. + address internal immutable RU_WETH; + + /// @notice Error for unsupported chain IDs. + error UnsupportedChain(uint256); + + constructor() { + if (block.chainid == PecorinoConstants.HOST_CHAIN_ID) { + PASSAGE = PecorinoConstants.HOST_PASSAGE; + ORDERS = PecorinoConstants.HOST_ORDERS; + + WETH = IERC20(PecorinoConstants.HOST_WETH); + WBTC = IERC20(PecorinoConstants.HOST_WBTC); + USDC = IERC20(PecorinoConstants.HOST_USDC); + USDT = IERC20(PecorinoConstants.HOST_USDT); + + RU_WUSD = address(PecorinoConstants.WUSD); + RU_WBTC = address(PecorinoConstants.WBTC); + RU_WETH = address(PecorinoConstants.WETH); + } else { + revert UnsupportedChain(block.chainid); + } + } + + /// @notice This is used to know whether to alias. It should generally + /// return false. If writing an ephemeral contract, override to return true. + function isEphemeral() internal pure virtual returns (bool) { + return false; + } + + /// @notice Returns + function selfOnL2() internal view returns (address) { + if (isEphemeral()) { + return address(this); + } + if (address(this).code.length == 23) { + bool is7702; + assembly { + let ptr := mload(0x40) + extcodecopy(caller(), ptr, 0, 0x20) + is7702 := eq(shr(232, mload(ptr)), 0xEF0100) + // clean the memory we used. Unnecessary, but good hygiene + mstore(ptr, 0x0) + } + if (is7702) { + return address(this); + } + } + return AddressAliasHelper.applyL1ToL2Alias(address(this)); + } + + function makeOutput(address token, uint256 amount, address recipient) + internal + pure + returns (HostOrders.Output memory output) + { + output.token = token; + output.amount = amount; + output.recipient = recipient; + output.chainId = PecorinoConstants.HOST_CHAIN_ID; + } + + function usdcOutput(uint256 amount, address recipient) internal view returns (HostOrders.Output memory output) { + return makeOutput(address(USDC), amount, recipient); + } + + function usdtOutput(uint256 amount, address recipient) internal view returns (HostOrders.Output memory output) { + return makeOutput(address(USDT), amount, recipient); + } + + function wbtcOutput(uint256 amount, address recipient) internal view returns (HostOrders.Output memory output) { + return makeOutput(address(WBTC), amount, recipient); + } + + function wethOutput(uint256 amount, address recipient) internal view returns (HostOrders.Output memory output) { + return makeOutput(address(WETH), amount, recipient); + } + + function ethOutput(uint256 amount, address recipient) internal pure returns (HostOrders.Output memory output) { + return makeOutput(NATIVE_ASSET, amount, recipient); + } + + function enterSignetToken(address token, uint256 amount) internal { + if (token == NATIVE_ASSET) { + enterSignetEth(amount); + return; + } + IERC20(token).approve(address(PASSAGE), amount); + PASSAGE.enterToken(selfOnL2(), token, amount); + } + + function enterSignetEth(uint256 amount) internal { + PASSAGE.enter{value: amount}(selfOnL2()); + } +} diff --git a/src/l2/Signet.sol b/src/l2/Signet.sol index 458b380..4b37300 100644 --- a/src/l2/Signet.sol +++ b/src/l2/Signet.sol @@ -55,7 +55,7 @@ contract SignetL2 { HOST_WBTC = PecorinoConstants.HOST_WBTC; HOST_WETH = PecorinoConstants.HOST_WETH; } else { - revert("Unsupported chain"); + revert UnsupportedChain(block.chainid); } } diff --git a/src/vendor/AddressAliasHelper.sol b/src/vendor/AddressAliasHelper.sol new file mode 100644 index 0000000..7bcdbb9 --- /dev/null +++ b/src/vendor/AddressAliasHelper.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2019-2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pragma solidity ^0.8.0; + +library AddressAliasHelper { + uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); + + /// @notice Utility function that converts the address in the L1 that submitted a tx to + /// the inbox to the msg.sender viewed in the L2 + /// @param l1Address the address in the L1 that triggered the tx to L2 + /// @return l2Address L2 address as viewed in msg.sender + function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { + unchecked { + l2Address = address(uint160(l1Address) + offset); + } + } + + /// @notice Utility function that converts the msg.sender viewed in the L2 to the + /// address in the L1 that submitted a tx to the inbox + /// @param l2Address L2 address as viewed in msg.sender + /// @return l1Address the address in the L1 that triggered the tx to L2 + function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { + unchecked { + l1Address = address(uint160(l2Address) - offset); + } + } +} From 8e83b72cf290d889924568483cc5a6dead688081 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 8 Dec 2025 10:55:46 -0500 Subject: [PATCH 2/5] feat: SignetL1Ephemeral --- src/l1/Signet.sol | 14 +++----------- src/l1/SignetL1Ephemeral.sol | 10 ++++++++++ 2 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 src/l1/SignetL1Ephemeral.sol diff --git a/src/l1/Signet.sol b/src/l1/Signet.sol index 0dee21a..b85bd2a 100644 --- a/src/l1/Signet.sol +++ b/src/l1/Signet.sol @@ -54,17 +54,9 @@ abstract contract SignetL1 { } } - /// @notice This is used to know whether to alias. It should generally - /// return false. If writing an ephemeral contract, override to return true. - function isEphemeral() internal pure virtual returns (bool) { - return false; - } - - /// @notice Returns - function selfOnL2() internal view returns (address) { - if (isEphemeral()) { - return address(this); - } + /// @notice Returns the address of this contract on L2, applying an + /// address alias. + function selfOnL2() internal view virtual returns (address) { if (address(this).code.length == 23) { bool is7702; assembly { diff --git a/src/l1/SignetL1Ephemeral.sol b/src/l1/SignetL1Ephemeral.sol new file mode 100644 index 0000000..8f0dfa6 --- /dev/null +++ b/src/l1/SignetL1Ephemeral.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {SignetL1} from "./Signet.sol"; + +abstract contract SignetL1Ephemeral is SignetL1 { + function selfOnL2() internal view override returns (address) { + return address(this); + } +} From 0b4af07b377ab7a0a71c465d7290c3b2af908cc4 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 8 Dec 2025 11:01:13 -0500 Subject: [PATCH 3/5] feat: wethToSignet --- src/interfaces/IWETH.sol | 12 ++++++++++++ src/l1/Signet.sol | 25 ++++++++++++++++++++----- test/Tests.sol | 5 ++++- 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 src/interfaces/IWETH.sol diff --git a/src/interfaces/IWETH.sol b/src/interfaces/IWETH.sol new file mode 100644 index 0000000..1f3af1b --- /dev/null +++ b/src/interfaces/IWETH.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >0.8.13; + +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +interface IWETH is IERC20 { + /// @notice Deposit ETH to get WETH + function deposit() external payable; + + /// @notice Withdraw WETH to get ETH + function withdraw(uint256 amount) external; +} diff --git a/src/l1/Signet.sol b/src/l1/Signet.sol index b85bd2a..3b4f77d 100644 --- a/src/l1/Signet.sol +++ b/src/l1/Signet.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.13; import {HostOrders} from "zenith/src/orders/HostOrders.sol"; import {Passage} from "zenith/src/passage/Passage.sol"; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IWETH} from "../interfaces/IWETH.sol"; import {AddressAliasHelper} from "../vendor/AddressAliasHelper.sol"; import {PecorinoConstants} from "../chains/Pecorino.sol"; @@ -18,7 +19,7 @@ abstract contract SignetL1 { HostOrders internal immutable ORDERS; /// @notice The WETH token address. - IERC20 internal immutable WETH; + IWETH internal immutable WETH; /// @notice The WBTC token address. IERC20 internal immutable WBTC; /// @notice The USDC token address. @@ -41,7 +42,7 @@ abstract contract SignetL1 { PASSAGE = PecorinoConstants.HOST_PASSAGE; ORDERS = PecorinoConstants.HOST_ORDERS; - WETH = IERC20(PecorinoConstants.HOST_WETH); + WETH = IWETH(PecorinoConstants.HOST_WETH); WBTC = IERC20(PecorinoConstants.HOST_WBTC); USDC = IERC20(PecorinoConstants.HOST_USDC); USDT = IERC20(PecorinoConstants.HOST_USDT); @@ -73,6 +74,7 @@ abstract contract SignetL1 { return AddressAliasHelper.applyL1ToL2Alias(address(this)); } + /// @notice Helper to create an output struct. function makeOutput(address token, uint256 amount, address recipient) internal pure @@ -84,36 +86,49 @@ abstract contract SignetL1 { output.chainId = PecorinoConstants.HOST_CHAIN_ID; } + /// @notice Helper to create an Output struct for usdc. function usdcOutput(uint256 amount, address recipient) internal view returns (HostOrders.Output memory output) { return makeOutput(address(USDC), amount, recipient); } + /// @notice Helper to create an Output struct for usdt. function usdtOutput(uint256 amount, address recipient) internal view returns (HostOrders.Output memory output) { return makeOutput(address(USDT), amount, recipient); } + /// @notice Helper to create an Output struct for wbtc. function wbtcOutput(uint256 amount, address recipient) internal view returns (HostOrders.Output memory output) { return makeOutput(address(WBTC), amount, recipient); } + /// @notice Helper to create an Output struct for weth. function wethOutput(uint256 amount, address recipient) internal view returns (HostOrders.Output memory output) { return makeOutput(address(WETH), amount, recipient); } + /// @notice Helper to create an Output struct for eth. function ethOutput(uint256 amount, address recipient) internal pure returns (HostOrders.Output memory output) { return makeOutput(NATIVE_ASSET, amount, recipient); } - function enterSignetToken(address token, uint256 amount) internal { + /// @notice Send tokens into Signet via the Passage contract. + function tokensToSignet(address token, uint256 amount) internal { if (token == NATIVE_ASSET) { - enterSignetEth(amount); + ethToSignet(amount); return; } IERC20(token).approve(address(PASSAGE), amount); PASSAGE.enterToken(selfOnL2(), token, amount); } - function enterSignetEth(uint256 amount) internal { + /// @notice Send ETH into Signet via the Passage contract. + function ethToSignet(uint256 amount) internal { PASSAGE.enter{value: amount}(selfOnL2()); } + + /// @notice Send WETH into Signet via the Passage contract. + function wethToSignet(uint256 amount) internal { + WETH.withdraw(amount); + ethToSignet(amount); + } } diff --git a/test/Tests.sol b/test/Tests.sol index 39b3aa2..9d2bf2b 100644 --- a/test/Tests.sol +++ b/test/Tests.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -contract TestNop { +import {Test} from "forge-std/Test.sol"; + +contract TestNop is Test { /// @notice Prevents foundry from complaining about no tests in CI. function test_nop() external pure returns (bool) { return true; } } + From f2a4f43ee4269a3e2fabbc11682ec5f8d61d2982 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 9 Dec 2025 10:12:22 -0500 Subject: [PATCH 4/5] fix: codecopy --- src/l1/Signet.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/l1/Signet.sol b/src/l1/Signet.sol index 3b4f77d..fd2c461 100644 --- a/src/l1/Signet.sol +++ b/src/l1/Signet.sol @@ -62,7 +62,7 @@ abstract contract SignetL1 { bool is7702; assembly { let ptr := mload(0x40) - extcodecopy(caller(), ptr, 0, 0x20) + codecopy(ptr, 0, 0x20) is7702 := eq(shr(232, mload(ptr)), 0xEF0100) // clean the memory we used. Unnecessary, but good hygiene mstore(ptr, 0x0) From 9dd29cf23d8abab61fdd72f582feb5c3a4f7f182 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 9 Dec 2025 10:14:47 -0500 Subject: [PATCH 5/5] chore: use forceApprove --- src/l1/Signet.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/l1/Signet.sol b/src/l1/Signet.sol index fd2c461..5377d5e 100644 --- a/src/l1/Signet.sol +++ b/src/l1/Signet.sol @@ -4,13 +4,16 @@ pragma solidity ^0.8.13; import {HostOrders} from "zenith/src/orders/HostOrders.sol"; import {Passage} from "zenith/src/passage/Passage.sol"; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {IWETH} from "../interfaces/IWETH.sol"; import {AddressAliasHelper} from "../vendor/AddressAliasHelper.sol"; import {PecorinoConstants} from "../chains/Pecorino.sol"; abstract contract SignetL1 { - /// @notice Sentinal value for the native asset in order inputs/outputs + using SafeERC20 for IERC20; + + /// @notice Sentinel value for the native asset in order inputs/outputs address constant NATIVE_ASSET = address(0); /// @notice The Passage address @@ -58,11 +61,13 @@ abstract contract SignetL1 { /// @notice Returns the address of this contract on L2, applying an /// address alias. function selfOnL2() internal view virtual returns (address) { - if (address(this).code.length == 23) { + address self = address(this); + if (self.code.length == 23) { bool is7702; + assembly { let ptr := mload(0x40) - codecopy(ptr, 0, 0x20) + extcodecopy(self, ptr, 0, 0x20) is7702 := eq(shr(232, mload(ptr)), 0xEF0100) // clean the memory we used. Unnecessary, but good hygiene mstore(ptr, 0x0) @@ -117,7 +122,7 @@ abstract contract SignetL1 { ethToSignet(amount); return; } - IERC20(token).approve(address(PASSAGE), amount); + IERC20(token).forceApprove(address(PASSAGE), amount); PASSAGE.enterToken(selfOnL2(), token, amount); }