diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 060cef2..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "solidity.compileUsingRemoteVersion": "v0.8.20+commit.a1b79de6", - "solidity.remappings": [ - "ds-test/=lib/forge-std/lib/ds-test/src/", - "forge-std/=lib/forge-std/src/", - "@layerzerolabs/lz-evm-oapp-v2/contracts/=node_modules/@layerzerolabs/lz-evm-oapp-v2/contracts/", - "@layerzerolabs/lz-evm-protocol-v2/contracts/=node_modules/@layerzerolabs/lz-evm-protocol-v2/contracts/", - "@layerzerolabs/lz-evm-messagelib-v2/contracts/=node_modules/@layerzerolabs/lz-evm-messagelib-v2/contracts/", - "@layerzerolabs/lz-evm-oapp-v2/contracts-upgradeable/=node_modules/layerzero-v2/oapp/contracts/" - ] -} diff --git a/audit/ScrollNativeMintingAuditReport.md b/audit/ScrollNativeMintingAuditReport.md new file mode 100644 index 0000000..fbec50e --- /dev/null +++ b/audit/ScrollNativeMintingAuditReport.md @@ -0,0 +1,15 @@ +# [NM-0217] Scroll Native Minting Request + +**File(s)**: [L1ScrollReceiverETHUpgradeable.sol](https://github.com/etherfi-protocol/weETH-cross-chain/blob/aa5fd7687686c67febe7f07c3f68da798ef3fd41/contracts/NativeMinting/ReceiverContracts/L1ScrollReceiverETHUpgradeable.sol), [L2OPStackSyncPoolETHUpgradeable.sol](https://github.com/etherfi-protocol/weETH-cross-chain/blob/8467b3903c71790c08f183bcbe8224bfb1c6b0b2/contracts/NativeMinting/L2SyncPoolContracts/L2OPStackSyncPoolETHUpgradeable.sol), [L2ScrollSyncPoolETHUpgradeable](https://github.com/etherfi-protocol/weETH-cross-chain/blob/b953a0260deef2f70ba556ff064d45b21d9bc894/contracts/NativeMinting/L2SyncPoolContracts/L2ScrollSyncPoolETHUpgradeable.sol#L108) + +### Summary + +This PR extends the cross chain functionality that allows users to natively mint weETH without the need to swap through a DEX by adding support for the Scroll blockchain. In order to add this feature, the smart contracts required slight customizations of the already existing code to integrate with Scroll. + +--- + +### Review conclusions + +After reviewing the updated code, we don't see any clear risk on the changes that were implemented. The code seems to work as expected. + +--- diff --git a/contracts/NativeMinting/L2SyncPoolContracts/EtherfiL2ModeSyncPoolETH.sol b/contracts/NativeMinting/L2SyncPoolContracts/EtherfiL2ModeSyncPoolETH.sol deleted file mode 100644 index d62087a..0000000 --- a/contracts/NativeMinting/L2SyncPoolContracts/EtherfiL2ModeSyncPoolETH.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {L2ModeSyncPoolETHUpgradeable} from "./L2ModeSyncPoolETHUpgradeable.sol"; - -contract EtherfiL2ModeSyncPoolETH is L2ModeSyncPoolETHUpgradeable { - constructor(address endpoint) L2ModeSyncPoolETHUpgradeable(endpoint) { - _disableInitializers(); - } - - function initialize( - address l2ExchangeRateProvider, - address rateLimiter, - address tokenOut, - uint32 dstEid, - address messenger, - address receiver, - address delegate - ) external override initializer { - __L2BaseSyncPool_init(l2ExchangeRateProvider, rateLimiter, tokenOut, dstEid, delegate); - __BaseMessenger_init(messenger); - __BaseReceiver_init(receiver); - __Ownable_init(delegate); - } -} diff --git a/contracts/NativeMinting/L2SyncPoolContracts/L2ModeSyncPoolETHUpgradeable.sol b/contracts/NativeMinting/L2SyncPoolContracts/L2OPStackSyncPoolETHUpgradeable.sol similarity index 79% rename from contracts/NativeMinting/L2SyncPoolContracts/L2ModeSyncPoolETHUpgradeable.sol rename to contracts/NativeMinting/L2SyncPoolContracts/L2OPStackSyncPoolETHUpgradeable.sol index 457848f..a6c396b 100644 --- a/contracts/NativeMinting/L2SyncPoolContracts/L2ModeSyncPoolETHUpgradeable.sol +++ b/contracts/NativeMinting/L2SyncPoolContracts/L2OPStackSyncPoolETHUpgradeable.sol @@ -14,22 +14,23 @@ import {Constants} from "../../libraries/Constants.sol"; import {IL1Receiver} from "../../../interfaces/IL1Receiver.sol"; /** - * @title L2 Mode Sync Pool for ETH - * @dev A sync pool that only supports ETH on Mode L2 + * @title L2 OP Stack Sync Pool for ETH + * @dev A sync pool that only supports ETH on OP Stack L2s * This contract allows to send ETH from L2 to L1 during the sync process */ -contract L2ModeSyncPoolETHUpgradeable is - L2BaseSyncPoolUpgradeable, - BaseMessengerUpgradeable, - BaseReceiverUpgradeable -{ - error L2ModeSyncPoolETH__OnlyETH(); +contract L2OPStackSyncPoolETHUpgradeable is L2BaseSyncPoolUpgradeable, BaseMessengerUpgradeable, BaseReceiverUpgradeable { + + event DepositWithReferral(address indexed sender, uint256 amount, address referral); + + error L2OPStackSyncPoolETH__OnlyETH(); /** - * @dev Constructor for L2 Mode Sync Pool for ETH + * @dev Constructor for L2 OP Stack Sync Pool for ETH * @param endpoint Address of the LayerZero endpoint */ - constructor(address endpoint) L2BaseSyncPoolUpgradeable(endpoint) {} + constructor(address endpoint) L2BaseSyncPoolUpgradeable(endpoint) { + _disableInitializers(); + } /** * @dev Initialize the contract @@ -62,7 +63,7 @@ contract L2ModeSyncPoolETHUpgradeable is * @param amountIn The amount of tokens */ function _receiveTokenIn(address tokenIn, uint256 amountIn) internal virtual override { - if (tokenIn != Constants.ETH_ADDRESS) revert L2ModeSyncPoolETH__OnlyETH(); + if (tokenIn != Constants.ETH_ADDRESS) revert L2OPStackSyncPoolETH__OnlyETH(); super._receiveTokenIn(tokenIn, amountIn); } @@ -89,7 +90,7 @@ contract L2ModeSyncPoolETHUpgradeable is MessagingFee calldata fee ) internal virtual override returns (MessagingReceipt memory) { if (l1TokenIn != Constants.ETH_ADDRESS || l2TokenIn != Constants.ETH_ADDRESS) { - revert L2ModeSyncPoolETH__OnlyETH(); + revert L2OPStackSyncPoolETH__OnlyETH(); } address receiver = getReceiver(); @@ -107,4 +108,17 @@ contract L2ModeSyncPoolETHUpgradeable is return receipt; } + + /** + * @dev Deposit function with referral event + */ + function deposit( + address tokenIn, + uint256 amountIn, + uint256 minAmountOut, + address referral + ) public payable returns (uint256 amountOut) { + emit DepositWithReferral(msg.sender, msg.value, referral); + return super.deposit(tokenIn, amountIn, minAmountOut); + } } diff --git a/contracts/NativeMinting/L2SyncPoolContracts/L2ScrollSyncPoolETHUpgradeable.sol b/contracts/NativeMinting/L2SyncPoolContracts/L2ScrollSyncPoolETHUpgradeable.sol new file mode 100644 index 0000000..60cdfa3 --- /dev/null +++ b/contracts/NativeMinting/L2SyncPoolContracts/L2ScrollSyncPoolETHUpgradeable.sol @@ -0,0 +1,126 @@ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { + MessagingFee, + MessagingReceipt +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import {BaseMessengerUpgradeable} from "../LayerZeroBaseContracts/BaseMessengerUpgradeable.sol"; +import {BaseReceiverUpgradeable} from "../LayerZeroBaseContracts/BaseReceiverUpgradeable.sol"; +import {L2BaseSyncPoolUpgradeable} from "../LayerZeroBaseContracts/L2BaseSyncPoolUpgradeable.sol"; +import {IL2ScrollMessenger} from "../../../interfaces/IL2ScrollMessenger.sol"; +import {Constants} from "../../libraries/Constants.sol"; +import {IL1Receiver} from "../../../interfaces/IL1Receiver.sol"; + +/** + * @title L2 Scroll Stack Sync Pool for ETH + * @dev A sync pool that only supports ETH on Scroll Stack L2s + * This contract allows to send ETH from L2 to L1 during the sync process + */ +contract L2ScrollSyncPoolETHUpgradeable is L2BaseSyncPoolUpgradeable, BaseMessengerUpgradeable, BaseReceiverUpgradeable { + + event DepositWithReferral(address indexed sender, uint256 amount, address referral); + + error L2ScrollStackSyncPoolETH__OnlyETH(); + + /** + * @dev Constructor for L2 Scroll Stack Sync Pool for ETH + * @param endpoint Address of the LayerZero endpoint + */ + constructor(address endpoint) L2BaseSyncPoolUpgradeable(endpoint) { + _disableInitializers(); + } + + /** + * @dev Initialize the contract + * @param l2ExchangeRateProvider Address of the exchange rate provider + * @param rateLimiter Address of the rate limiter + * @param tokenOut Address of the token to mint on Layer 2 + * @param dstEid Destination endpoint ID (most of the time, the Layer 1 endpoint ID) + * @param messenger Address of the messenger contract (most of the time, the L2 native bridge address) + * @param receiver Address of the receiver contract (most of the time, the L1 receiver contract) + * @param delegate Address of the owner + */ + function initialize( + address l2ExchangeRateProvider, + address rateLimiter, + address tokenOut, + uint32 dstEid, + address messenger, + address receiver, + address delegate + ) external virtual initializer { + + __L2BaseSyncPool_init(l2ExchangeRateProvider, rateLimiter, tokenOut, dstEid, delegate); + __BaseMessenger_init(messenger); + __BaseReceiver_init(receiver); + __Ownable_init(delegate); + } + + /** + * @dev Only allows ETH to be received + * @param tokenIn The token address + * @param amountIn The amount of tokens + */ + function _receiveTokenIn(address tokenIn, uint256 amountIn) internal virtual override { + if (tokenIn != Constants.ETH_ADDRESS) revert L2ScrollStackSyncPoolETH__OnlyETH(); + + super._receiveTokenIn(tokenIn, amountIn); + } + + /** + * @dev Internal function to sync tokens to L1 + * This will send an additional message to the messenger contract after the LZ message + * This message will contain the ETH that the LZ message anticipates to receive + * @param dstEid Destination endpoint ID + * @param l1TokenIn Address of the token on Layer 1 + * @param amountIn Amount of tokens deposited on Layer 2 + * @param amountOut Amount of tokens minted on Layer 2 + * @param extraOptions Extra options for the messaging protocol + * @param fee Messaging fee + * @return receipt Messaging receipt + */ + function _sync( + uint32 dstEid, + address l2TokenIn, + address l1TokenIn, + uint256 amountIn, + uint256 amountOut, + bytes calldata extraOptions, + MessagingFee calldata fee + ) internal virtual override returns (MessagingReceipt memory) { + if (l1TokenIn != Constants.ETH_ADDRESS || l2TokenIn != Constants.ETH_ADDRESS) { + revert L2ScrollStackSyncPoolETH__OnlyETH(); + } + + address receiver = getReceiver(); + address messenger = getMessenger(); + + uint32 originEid = endpoint.eid(); + + MessagingReceipt memory receipt = + super._sync(dstEid, l2TokenIn, l1TokenIn, amountIn, amountOut, extraOptions, fee); + + bytes memory data = abi.encode(originEid, receipt.guid, l1TokenIn, amountIn, amountOut); + bytes memory message = abi.encodeCall(IL1Receiver.onMessageReceived, data); + + IL2ScrollMessenger(messenger).sendMessage{value: amountIn}(receiver, amountIn, message, 0); + + return receipt; + } + + /** + * @dev Deposit function with referral event + */ + function deposit( + address tokenIn, + uint256 amountIn, + uint256 minAmountOut, + address referral + ) public payable returns (uint256 amountOut) { + emit DepositWithReferral(msg.sender, msg.value, referral); + return super.deposit(tokenIn, amountIn, minAmountOut); + } +} diff --git a/contracts/NativeMinting/ReceiverContracts/L1ScrollReceiverETHUpgradeable.sol b/contracts/NativeMinting/ReceiverContracts/L1ScrollReceiverETHUpgradeable.sol new file mode 100644 index 0000000..b41ed31 --- /dev/null +++ b/contracts/NativeMinting/ReceiverContracts/L1ScrollReceiverETHUpgradeable.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {L1BaseReceiverUpgradeable} from "../LayerZeroBaseContracts/L1BaseReceiverUpgradeable.sol"; +import {IL1ScrollMessenger} from "../../../interfaces/IL1ScrollMessenger.sol"; +import {Constants} from "../../libraries/Constants.sol"; + +/** + * @title L1 Scroll Receiver ETH + * @notice L1 receiver contract for ETH + * @dev This contract receives messages from the scroll L2 messenger and forwards them to the L1 sync pool + * It only supports ETH + */ +contract L1ScrollReceiverETHUpgradeable is L1BaseReceiverUpgradeable { + error L1ScrollReceiverETH__OnlyETH(); + + constructor() { + _disableInitializers(); + } + + /** + * @dev Initializer for L1 Mode Receiver ETH + * @param l1SyncPool Address of the L1 sync pool + * @param messenger Address of the messenger contract + * @param owner Address of the owner + */ + function initialize(address l1SyncPool, address messenger, address owner) external initializer { + __Ownable_init(owner); + __L1BaseReceiver_init(l1SyncPool, messenger); + } + + /** + * @dev Function to receive messages from the L2 messenger + * @param message The message received from the L2 messenger + */ + function onMessageReceived(bytes calldata message) external payable virtual override { + (uint32 originEid, bytes32 guid, address tokenIn, uint256 amountIn, uint256 amountOut) = + abi.decode(message, (uint32, bytes32, address, uint256, uint256)); + + if (tokenIn != Constants.ETH_ADDRESS) revert L1ScrollReceiverETH__OnlyETH(); + + address sender = IL1ScrollMessenger(getMessenger()).xDomainMessageSender(); + + _forwardToL1SyncPool( + originEid, bytes32(uint256(uint160(sender))), guid, tokenIn, amountIn, amountOut, msg.value + ); + } +} diff --git a/foundry.toml b/foundry.toml index a273639..96f29cd 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,7 +8,8 @@ fs_permissions = [{ access = "read-write", path = "./"}] optimizer = true optimizer_runs = 200 -solc_version = "0.8.20" +via_ir = true +solc_version = "0.8.24" # remove the hash of the metadata for more deterministic code bytecode_hash = 'none' diff --git a/interfaces/IL2ScrollMessenger.sol b/interfaces/IL2ScrollMessenger.sol new file mode 100644 index 0000000..6728569 --- /dev/null +++ b/interfaces/IL2ScrollMessenger.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +import {IScrollMessenger} from "./IScrollMessenger.sol"; + +interface IL2ScrollMessenger is IScrollMessenger { + /********** + * Events * + **********/ + + /// @notice Emitted when the maximum number of times each message can fail in L2 is updated. + /// @param oldMaxFailedExecutionTimes The old maximum number of times each message can fail in L2. + /// @param newMaxFailedExecutionTimes The new maximum number of times each message can fail in L2. + event UpdateMaxFailedExecutionTimes(uint256 oldMaxFailedExecutionTimes, uint256 newMaxFailedExecutionTimes); + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @notice execute L1 => L2 message + /// @dev Make sure this is only called by privileged accounts. + /// @param from The address of the sender of the message. + /// @param to The address of the recipient of the message. + /// @param value The msg.value passed to the message call. + /// @param nonce The nonce of the message to avoid replay attack. + /// @param message The content of the message. + function relayMessage( + address from, + address to, + uint256 value, + uint256 nonce, + bytes calldata message + ) external; +} diff --git a/scripts/AdapterMigration/01_DeployUpgradeableAdapter.s.sol b/scripts/AdapterMigration/01_DeployUpgradeableAdapter.s.sol index 5dff005..81015c4 100644 --- a/scripts/AdapterMigration/01_DeployUpgradeableAdapter.s.sol +++ b/scripts/AdapterMigration/01_DeployUpgradeableAdapter.s.sol @@ -9,13 +9,16 @@ import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.so import "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; -import "@layerzerolabs/lz-evm-oapp-v2/contracts-upgradeable/oapp/interfaces/IOAppOptionsType3.sol"; +// import "@layerzerolabs/lz-evm-oapp-v2/contracts-upgradeable/oapp/interfaces/IOAppOptionsType3.sol"; import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol"; -import "../../contracts/EtherfiOFTAdapterUpgradeable.sol"; -import "../../utils/Constants.sol"; +import "../../contracts/EtherFiOFTAdapterUpgradeable.sol"; +import "../../utils/L2Constants.sol"; + + import "../../utils/LayerZeroHelpers.sol"; -contract DeployUpgradeableOFTAdapter is Script, Constants, LayerZeroHelpers { + +contract DeployUpgradeableOFTAdapter is Script, L2Constants, LayerZeroHelpers { using OptionsBuilder for bytes; EnforcedOptionParam[] public enforcedOptions; @@ -60,7 +63,7 @@ contract DeployUpgradeableOFTAdapter is Script, Constants, LayerZeroHelpers { for (uint256 i = 0; i < L2s.length; i++) { _appendEnforcedOptions(L2s[i].L2_EID); } - adapter.setEnforcedOptions(enforcedOptions); + IOAppOptionsType3(adapterProxy).setEnforcedOptions(enforcedOptions); console.log("Transfering ownership to the gnosis..."); adapter.setDelegate(L1_CONTRACT_CONTROLLER); diff --git a/scripts/AdapterMigration/02_DeployMigrationOFT.s.sol b/scripts/AdapterMigration/02_DeployMigrationOFT.s.sol index 4e72845..5577ca1 100644 --- a/scripts/AdapterMigration/02_DeployMigrationOFT.s.sol +++ b/scripts/AdapterMigration/02_DeployMigrationOFT.s.sol @@ -10,11 +10,11 @@ import { EnforcedOptionParam } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oap import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol"; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "../../contracts/archive/MigrationOFT.sol"; -import "../../utils/Constants.sol"; +import "../../utils/L2Constants.sol"; import "../../utils/LayerZeroHelpers.sol"; -contract DeployMigrationOFT is Script, Constants, LayerZeroHelpers { +contract DeployMigrationOFT is Script, L2Constants, LayerZeroHelpers { using OptionsBuilder for bytes; address public migrationOFTAddress; diff --git a/scripts/AdapterMigration/03_MigrationTransactions.s.sol b/scripts/AdapterMigration/03_MigrationTransactions.s.sol index 93698a6..3e80695 100644 --- a/scripts/AdapterMigration/03_MigrationTransactions.s.sol +++ b/scripts/AdapterMigration/03_MigrationTransactions.s.sol @@ -12,7 +12,7 @@ import "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManage import "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/utils/RateLimiter.sol"; import "../../contracts/MintableOFTUpgradeable.sol"; -import "../../utils/Constants.sol"; +import "../../utils/L2Constants.sol"; import "../../utils/LayerZeroHelpers.sol"; contract GenerationMigrationTransactions is Script, Constants, LayerZeroHelpers { diff --git a/scripts/NativeMintingDeployment/DeployConfigureL1.s.sol b/scripts/NativeMintingDeployment/DeployConfigureL1.s.sol new file mode 100644 index 0000000..66ace76 --- /dev/null +++ b/scripts/NativeMintingDeployment/DeployConfigureL1.s.sol @@ -0,0 +1,98 @@ + +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/console.sol"; +import "forge-std/Script.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "../../contracts/NativeMinting/ReceiverContracts/L1ScrollReceiverETHUpgradeable.sol"; +import "../../contracts/NativeMinting/DummyTokenUpgradeable.sol"; +import "../../utils/GnosisHelpers.sol"; +import "../../utils/L2Constants.sol"; +import "../../utils/LayerZeroHelpers.sol"; + +contract L1NativeMintingScript is Script, L2Constants, LayerZeroHelpers, GnosisHelpers { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + function run() public { + + // vm.startBroadcast(DEPLOYER_ADDRESS); + + console.log("Deploying contracts on L1..."); + + address dummyTokenImpl = address(new DummyTokenUpgradeable{salt: keccak256("ScrollDummyTokenImpl")}(18)); + address dummyTokenProxy = address( + new TransparentUpgradeableProxy{salt: keccak256("ScrollDummyToken")}( + dummyTokenImpl, + L1_TIMELOCK, + abi.encodeWithSelector( + DummyTokenUpgradeable.initialize.selector, "Scroll Dummy ETH", "scrollETH", DEPLOYER_ADDRESS + ) + ) + ); + console.log("DummyToken deployed at: ", dummyTokenProxy); + require(dummyTokenProxy == SCROLL.L1_DUMMY_TOKEN, "Dummy Token address mismatch"); + + DummyTokenUpgradeable dummyToken = DummyTokenUpgradeable(dummyTokenProxy); + dummyToken.grantRole(MINTER_ROLE, L1_SYNC_POOL); + dummyToken.grantRole(DEFAULT_ADMIN_ROLE, L1_CONTRACT_CONTROLLER); + dummyToken.renounceRole(DEFAULT_ADMIN_ROLE, DEPLOYER_ADDRESS); + + address scrollReceiverImpl = address(new L1ScrollReceiverETHUpgradeable{salt: keccak256("ScrollReceiverImpl")}()); + address scrollReceiverProxy = address( + new TransparentUpgradeableProxy{salt: keccak256("ScrollReceiver")}( + scrollReceiverImpl, + L1_TIMELOCK, + abi.encodeWithSelector( + L1ScrollReceiverETHUpgradeable.initialize.selector, L1_SYNC_POOL, SCROLL.L1_MESSENGER, L1_CONTRACT_CONTROLLER + ) + ) + ); + console.log("ScrollReceiver deployed at: ", scrollReceiverProxy); + require(scrollReceiverProxy == SCROLL.L1_RECEIVER, "ScrollReceiver address mismatch"); + + console.log("Generating L1 transactions for native minting..."); + + // the require transactions to integrate native minting on the L1 side are spilt between the timelock and the L1 contract controller + + // 1. generate the schedule and execute transactions for the L1 sync pool + string memory timelock_schedule_transactions = _getGnosisHeader("1"); + string memory timelock_execute_transactions = _getGnosisHeader("1"); + + // registers the new dummy token as an acceptable token for the vamp contract + bytes memory setTokenData = abi.encodeWithSignature("registerToken(address,address,bool,uint16,uint32,uint32,bool)", dummyTokenProxy, address(0), true, 0, 20_000, 200_000, true); + timelock_schedule_transactions = string.concat(timelock_schedule_transactions, _getGnosisScheduleTransaction(L1_VAMP, setTokenData, false)); + timelock_execute_transactions = string.concat(timelock_execute_transactions, _getGnosisExecuteTransaction(L1_VAMP, setTokenData, false)); + + // set {receiver, dummy} token on the L1 sync pool + bytes memory setReceiverData = abi.encodeWithSignature("setReceiver(uint32,address)", SCROLL.L2_EID, scrollReceiverProxy); + bytes memory setDummyTokenData = abi.encodeWithSignature("setDummyToken(uint32,address)", SCROLL.L2_EID, dummyTokenProxy); + timelock_schedule_transactions = string.concat(timelock_schedule_transactions, _getGnosisScheduleTransaction(L1_SYNC_POOL, setReceiverData, false)); + timelock_schedule_transactions = string.concat(timelock_schedule_transactions, _getGnosisScheduleTransaction(L1_SYNC_POOL, setDummyTokenData, false)); + timelock_execute_transactions = string.concat(timelock_execute_transactions, _getGnosisExecuteTransaction(L1_SYNC_POOL, setReceiverData, false)); + timelock_execute_transactions = string.concat(timelock_execute_transactions, _getGnosisExecuteTransaction(L1_SYNC_POOL, setDummyTokenData, false)); + + // set OFT peer to scroll L2 sync pool that require the timelock for the L1 sync pool + bytes memory setPeerData = abi.encodeWithSignature("setPeer(uint32,bytes32)", SCROLL.L2_EID, _toBytes32(SCROLL.L2_SYNC_POOL)); + timelock_schedule_transactions = string.concat(timelock_schedule_transactions, _getGnosisScheduleTransaction(L1_SYNC_POOL, setPeerData, false)); + timelock_execute_transactions = string.concat(timelock_execute_transactions, _getGnosisExecuteTransaction(L1_SYNC_POOL, setPeerData, false)); + + // TODO: remove this transaction after the scroll native minting upgrade. It is a one time call to transfer the LZ delegate for the L1 sync pool from the deployer EOA to the L1 contract controller + bytes memory setDelegate = abi.encodeWithSignature("setDelegate(address)", L1_CONTRACT_CONTROLLER); + timelock_schedule_transactions = string.concat(timelock_schedule_transactions, _getGnosisScheduleTransaction(L1_SYNC_POOL, setDelegate, true)); + timelock_execute_transactions = string.concat(timelock_execute_transactions, _getGnosisExecuteTransaction(L1_SYNC_POOL, setDelegate, true)); + + vm.writeJson(timelock_schedule_transactions, "./output/L1NativeMintingScheduleTransactions.json"); + vm.writeJson(timelock_execute_transactions, "./output/L1NativeMintingExecuteTransactions.json"); + + // 2. generate transactions required by the L1 contract controller + string memory l1_contract_controller_transaction = _getGnosisHeader("1"); + + // set DVN receive config for the L1 sync to receive messages from the L2 sync pool + string memory setLZConfigReceive = iToHex(abi.encodeWithSignature("setConfig(address,address,(uint32,uint32,bytes)[])", L1_SYNC_POOL, L1_RECEIVE_302, getDVNConfig(SCROLL.L2_EID, L1_DVN))); + l1_contract_controller_transaction = string.concat(l1_contract_controller_transaction, _getGnosisTransaction(iToHex(abi.encodePacked(L1_ENDPOINT)), setLZConfigReceive, true)); + + vm.writeJson(l1_contract_controller_transaction, "./output/L1NativeMintingSetConfig.json"); + } +} diff --git a/scripts/NativeMintingDeployment/DeployConfigureL2.s.sol b/scripts/NativeMintingDeployment/DeployConfigureL2.s.sol new file mode 100644 index 0000000..f86d75c --- /dev/null +++ b/scripts/NativeMintingDeployment/DeployConfigureL2.s.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/console.sol"; +import "forge-std/Script.sol"; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/interfaces/IOAppOptionsType3.sol"; +import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; +import "../../contracts/NativeMinting/EtherfiL2ExchangeRateProvider.sol"; +import "../../contracts/NativeMinting/L2SyncPoolContracts/L2ScrollSyncPoolETHUpgradeable.sol"; +import "../../contracts/NativeMinting/BucketRateLimiter.sol"; + +import "../../utils/L2Constants.sol"; +import "../../utils/LayerZeroHelpers.sol"; +import "../../utils/GnosisHelpers.sol"; + +contract L2NativeMintingScript is Script, L2Constants, LayerZeroHelpers, GnosisHelpers { + using OptionsBuilder for bytes; + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + EnforcedOptionParam[] public enforcedOptions; + + function deployConfigureExchangeRateProvider(address scriptDeployer) private returns (address) { + address impl = address(new EtherfiL2ExchangeRateProvider{salt: keccak256("ExchangeRateProviderImpl")}()); + address proxy = address( + new TransparentUpgradeableProxy{salt: keccak256("ExchangeRateProvider")}( + impl, + SCROLL.L2_CONTRACT_CONTROLLER_SAFE, + abi.encodeWithSelector( + EtherfiL2ExchangeRateProvider.initialize.selector, + scriptDeployer + ) + ) + ); + console.log("Exchange Rate Provider deployed at: ", proxy); + require(proxy == SCROLL.L2_EXCHANGE_RATE_PROVIDER, "Exchange Rate Provider address mismatch"); + + EtherfiL2ExchangeRateProvider provider = EtherfiL2ExchangeRateProvider(proxy); + provider.setRateParameters(ETH_ADDRESS, SCROLL.L2_PRICE_ORACLE, 0, L2_PRICE_ORACLE_HEART_BEAT); + provider.transferOwnership(SCROLL.L2_CONTRACT_CONTROLLER_SAFE); + + return proxy; + } + + function deployConfigureBucketRateLimiter(address scriptDeployer) private returns (address) { + address impl = address(new BucketRateLimiter{salt: keccak256("BucketRateLimiterImpl")}()); + ERC1967Proxy proxy = new ERC1967Proxy{salt: keccak256("BucketRateLimiter")}( + impl, + abi.encodeWithSelector(BucketRateLimiter.initialize.selector, scriptDeployer) + ); + console.log("Bucket Rate Limiter deployed at: ", address(proxy)); + require(address(proxy) == SCROLL.L2_SYNC_POOL_RATE_LIMITER, "Bucket Rate Limiter address mismatch"); + + BucketRateLimiter limiter = BucketRateLimiter(address(proxy)); + limiter.setCapacity(BUCKET_SIZE); + limiter.setRefillRatePerSecond(BUCKET_REFILL_PER_SECOND); + limiter.updateConsumer(SCROLL.L2_SYNC_POOL); + limiter.transferOwnership(SCROLL.L2_CONTRACT_CONTROLLER_SAFE); + + return address(proxy); + } + + function deployConfigureSyncPool( + address scriptDeployer, + address exchangeRateProvider, + address bucketRateLimiter + ) private returns (address) { + address impl = address(new L2ScrollSyncPoolETHUpgradeable{salt: keccak256("L2SyncPoolImpl")}(SCROLL.L2_ENDPOINT)); + address proxy = address( + new TransparentUpgradeableProxy{salt: keccak256("L2SyncPool")}( + impl, + SCROLL.L2_CONTRACT_CONTROLLER_SAFE, + abi.encodeWithSelector( + L2ScrollSyncPoolETHUpgradeable.initialize.selector, + exchangeRateProvider, + bucketRateLimiter, + SCROLL.L2_OFT, + L1_EID, + SCROLL.L2_MESSENGER, + SCROLL.L1_RECEIVER, + scriptDeployer + ) + ) + ); + + console.log("Sync Pool deployed at: ", proxy); + require(proxy == SCROLL.L2_SYNC_POOL, "Sync Pool address mismatch"); + + L2ScrollSyncPoolETHUpgradeable syncPool = L2ScrollSyncPoolETHUpgradeable(proxy); + + // set all LayerZero configurations and sync pool specific configurations + syncPool.setPeer(L1_EID, _toBytes32(L1_SYNC_POOL)); + IOAppOptionsType3(proxy).setEnforcedOptions(getEnforcedOptions(L1_EID)); + ILayerZeroEndpointV2(SCROLL.L2_ENDPOINT).setConfig( + address(syncPool), + SCROLL.SEND_302, + getDVNConfig(L1_EID, SCROLL.LZ_DVN) + ); + + syncPool.setL1TokenIn(Constants.ETH_ADDRESS, Constants.ETH_ADDRESS); + syncPool.transferOwnership(SCROLL.L2_CONTRACT_CONTROLLER_SAFE); + + return proxy; + } + + function run() public { + vm.startBroadcast(DEPLOYER_ADDRESS); + + console.log("Deploying contracts on L2..."); + + // deploy and configure the native minting related contracts + address exchangeRateProvider = deployConfigureExchangeRateProvider(DEPLOYER_ADDRESS); + address rateLimiter = deployConfigureBucketRateLimiter(DEPLOYER_ADDRESS); + deployConfigureSyncPool(DEPLOYER_ADDRESS, exchangeRateProvider, rateLimiter); + + // generate the transactions required by the L2 contract controller + + // give the L2 sync pool permission to mint the dummy token + string memory minterTransaction = _getGnosisHeader(SCROLL.CHAIN_ID); + bytes memory setMinterData = abi.encodeWithSignature("grantRole(bytes32,address)", MINTER_ROLE, SCROLL.L2_SYNC_POOL); + minterTransaction = string.concat(minterTransaction, _getGnosisTransaction(iToHex(abi.encodePacked(SCROLL.L2_OFT)), iToHex(setMinterData), true)); + vm.writeJson(minterTransaction, "./output/setScrollMinter.json"); + + // transaction to set the min sync + string memory minSyncTransaction = _getGnosisHeader(SCROLL.CHAIN_ID); + bytes memory setMinSyncData = abi.encodeWithSignature("setMinSyncAmount(address,uint256)", Constants.ETH_ADDRESS, 10 ether); + minSyncTransaction = string.concat(minSyncTransaction, _getGnosisTransaction(iToHex(abi.encodePacked(SCROLL.L2_SYNC_POOL)), iToHex(setMinSyncData), true)); + vm.writeJson(minSyncTransaction, "./output/setMinSyncAmount.json"); + } +} diff --git a/scripts/OFTDeployment/01_OFTConfigure.s.sol b/scripts/OFTDeployment/01_OFTConfigure.s.sol index 7a85f7e..f960524 100644 --- a/scripts/OFTDeployment/01_OFTConfigure.s.sol +++ b/scripts/OFTDeployment/01_OFTConfigure.s.sol @@ -13,7 +13,7 @@ import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/lib import "../../contracts/PairwiseRateLimiter.sol"; import "../../contracts/EtherfiOFTUpgradeable.sol"; -import "../../utils/Constants.sol"; +import "../../utils/L2Constants.sol"; import "../../utils/LayerZeroHelpers.sol"; struct OFTDeployment { diff --git a/scripts/OFTDeployment/02_UpdateOFTPeersTransactions.s.sol b/scripts/OFTDeployment/02_UpdateOFTPeersTransactions.s.sol index 1e090e2..5b837dd 100644 --- a/scripts/OFTDeployment/02_UpdateOFTPeersTransactions.s.sol +++ b/scripts/OFTDeployment/02_UpdateOFTPeersTransactions.s.sol @@ -13,7 +13,7 @@ import "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/utils/RateLimiter.sol"; import "../../contracts/PairwiseRateLimiter.sol"; import "../../contracts/EtherfiOFTUpgradeable.sol"; -import "../../utils/Constants.sol"; +import "../../utils/L2Constants.sol"; import "../../utils/LayerZeroHelpers.sol"; diff --git a/scripts/OFTDeployment/03_OFTOwnershipTransfer.s.sol b/scripts/OFTDeployment/03_OFTOwnershipTransfer.s.sol index de4da4b..d7f4572 100644 --- a/scripts/OFTDeployment/03_OFTOwnershipTransfer.s.sol +++ b/scripts/OFTDeployment/03_OFTOwnershipTransfer.s.sol @@ -16,7 +16,7 @@ import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/lib import "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol"; import "../../contracts/EtherfiOFTUpgradeable.sol"; -import "../../utils/Constants.sol"; +import "../../utils/L2Constants.sol"; // forge script scripts/OFTDeployment/03_OFTOwnershipTransfer.s.sol:OFTOwnershipTransfer --rpc-url "deployment rpc" --ledger contract OFTOwnershipTransfer is Script, Constants { diff --git a/scripts/OFTDeployment/04_OFTSend.s.sol b/scripts/OFTDeployment/04_OFTSend.s.sol index d5a4f46..1550e17 100644 --- a/scripts/OFTDeployment/04_OFTSend.s.sol +++ b/scripts/OFTDeployment/04_OFTSend.s.sol @@ -17,7 +17,7 @@ import "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol"; import "../../contracts/EtherfiOFTUpgradeable.sol"; import "forge-std/Test.sol"; -import "../../utils/Constants.sol"; +import "../../utils/L2Constants.sol"; import "../../utils/LayerZeroHelpers.sol"; // forge script scripts/OFTDeployment/04_OFTSend.s.sol:CrossChainSend --rpc-url "source chain" --private-key "dev wallet" diff --git a/scripts/OFTDeployment/05_ProdRateLimit.sol b/scripts/OFTDeployment/05_ProdRateLimit.sol new file mode 100644 index 0000000..eea0b24 --- /dev/null +++ b/scripts/OFTDeployment/05_ProdRateLimit.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; + +import "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/utils/RateLimiter.sol"; + +import "../../contracts/MintableOFTUpgradeable.sol"; +import "../../utils/L2Constants.sol"; +import "../../utils/LayerZeroHelpers.sol"; + +contract SetRateLimits is Script, Constants, LayerZeroHelpers { + RateLimiter.RateLimitConfig[] public deploymentRateLimitConfigs; + + function run() public { + address scriptDeployer; + + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + scriptDeployer = vm.addr(privateKey); + + vm.startBroadcast(privateKey); + + // Address of the WEETH token + MintableOFTUpgradeable oft = new MintableOFTUpgradeable(DEPLOYMENT_OFT); + + // Set rate limits for L1 + deploymentRateLimitConfigs.push(_getRateLimitConfig(L1_EID, LIMIT, WINDOW)); + + // Iterate over each L2 and get the rate limit config + for (uint256 i = 0; i < L2s.length; i++) { + deploymentRateLimitConfigs.push(_getRateLimitConfig(L2s[i].L2_EID, LIMIT, WINDOW)); + } + + oft.setRateLimits(deploymentRateLimitConfigs); + } +} diff --git a/scripts/updateOFTRateLimt.s.sol b/scripts/updateOFTRateLimt.s.sol index f9c5d38..0250e13 100644 --- a/scripts/updateOFTRateLimt.s.sol +++ b/scripts/updateOFTRateLimt.s.sol @@ -6,7 +6,7 @@ import "forge-std/Script.sol"; import "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/utils/RateLimiter.sol"; import "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/utils/RateLimiter.sol"; -import "../utils/Constants.sol"; +import "../utils/L2Constants.sol"; import "../utils/LayerZeroHelpers.sol"; import "../utils/GnosisHelpers.sol"; diff --git a/test/AdapterMigration.t.sol b/test/AdapterMigration.t.sol index 38ed9f9..5d0b6cd 100644 --- a/test/AdapterMigration.t.sol +++ b/test/AdapterMigration.t.sol @@ -13,7 +13,7 @@ import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import "../scripts/AdapterMigration/01_DeployUpgradeableAdapter.s.sol" as DeployOFTAdapter; import "../scripts/AdapterMigration/02_DeployMigrationOFT.s.sol" as DeployMigrationOFT; -import "../utils/Constants.sol"; +import "../utils/L2Constants.sol"; import "../utils/LayerZeroHelpers.sol"; import "../contracts/archive/MigrationOFT.sol"; import "../contracts/EtherFiOFTAdapter.sol"; @@ -27,7 +27,7 @@ interface EndpointDelegates { function delegates(address) external view returns (address); } -contract OFTMigrationUnitTests is Test, Constants, LayerZeroHelpers { +contract OFTMigrationUnitTests is Test, L2Constants, LayerZeroHelpers { address constant DEPLOYMENT_OFT_ADAPTER = 0xcd2eb13D6831d4602D80E5db9230A57596CDCA63; diff --git a/test/OFTDeployment.t.sol b/test/OFTDeployment.t.sol index 0f6ead1..ad01c41 100644 --- a/test/OFTDeployment.t.sol +++ b/test/OFTDeployment.t.sol @@ -8,7 +8,6 @@ import "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManage import "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; import "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/utils/RateLimiter.sol"; -import "@layerzerolabs/lz-evm-oapp-v2/contracts-upgradeable/oapp/interfaces/IOAppOptionsType3.sol"; import "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol"; import { MessagingFee } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol"; @@ -16,12 +15,12 @@ import { SendParam } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interface import "../contracts/EtherfiOFTUpgradeable.sol"; import "../contracts/EtherFiOFTAdapter.sol"; -import "../utils/Constants.sol"; +import "../utils/L2Constants.sol"; import "../utils/LayerZeroHelpers.sol"; import "forge-std/Test.sol"; -contract OFTDeploymentTest is Test, Constants, LayerZeroHelpers { +contract OFTDeploymentTest is Test, L2Constants, LayerZeroHelpers { using OptionsBuilder for bytes; function testGnosisMainnet() public { diff --git a/test/nativeMintingScroll.t.sol b/test/nativeMintingScroll.t.sol new file mode 100644 index 0000000..40c0759 --- /dev/null +++ b/test/nativeMintingScroll.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "../scripts/NativeMintingDeployment/DeployConfigureL1.s.sol"; +import "../scripts/NativeMintingDeployment/DeployConfigureL2.s.sol"; +import "../contracts/NativeMinting/EtherfiL1SyncPoolETH.sol"; +import "../contracts/NativeMinting/L2SyncPoolContracts/L2ScrollSyncPoolETHUpgradeable.sol"; +import {Origin} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import "../contracts/NativeMinting/BucketRateLimiter.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../interfaces/IScrollMessenger.sol"; +import "../utils/L2Constants.sol"; +import "../utils/GnosisHelpers.sol"; +import "../utils/LayerZeroHelpers.sol"; +import "../utils/AppendOnlyMerkleTree.sol"; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +/** + * @title Native Minting Unit Tests + * @notice Test suite for verifying native minting functionality across L1 and L2 + */ +contract NativeMintingUnitTests is Test, L2Constants, GnosisHelpers, LayerZeroHelpers { + // Events for verifying bridge messages + event SentMessage( + address indexed sender, + address indexed target, + uint256 value, + uint256 messageNonce, + uint256 gasLimit, + bytes message + ); + + // Canonical bridge message expected values + address private SENDER = SCROLL.L2_SYNC_POOL; + address private TARGET = SCROLL.L1_RECEIVER; + uint256 private MESSAGE_VALUE = 1 ether; + bytes private BRIDGE_MESSAGE = hex"3a69197e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000007606ebd50bcf19f47f644e6981a58d2287a3b8d6c0702ffa0a1cb9ecdd12c568a498000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000d2ddfc66b17a973"; + + /// @notice Test the upgrade to natvie minting functionalilty and deposit/sync on L2 + function testNativeMintingL2() public { + // Setup L2 environment + vm.createSelectFork(SCROLL.RPC_URL); + L2NativeMintingScript nativeMintingL2 = new L2NativeMintingScript(); + // contracts have already been deployed hence no need to simulate deployments + // nativeMintingL2.run(); + + executeGnosisTransactionBundle("./output/setScrollMinter.json", SCROLL.L2_CONTRACT_CONTROLLER_SAFE); + vm.warp(block.timestamp + 3600); + + // Test deposit functionality + L2ScrollSyncPoolETHUpgradeable syncPool = L2ScrollSyncPoolETHUpgradeable(SCROLL.L2_SYNC_POOL); + address user = vm.addr(2); + startHoax(user); + syncPool.deposit{value: 1 ether}(Constants.ETH_ADDRESS, MESSAGE_VALUE, 0.90 ether); + + assertApproxEqAbs(IERC20(SCROLL.L2_OFT).balanceOf(user), 0.95 ether, 0.01 ether); + assertEq(address(syncPool).balance, 1 ether); + + // Test sync functionality + MessagingFee memory msgFee = syncPool.quoteSync(Constants.ETH_ADDRESS, hex"", false); + uint256 messageNonce = AppendOnlyMerkleTree(0x5300000000000000000000000000000000000000).nextMessageIndex(); + + vm.expectEmit(true, true, false, true); + emit SentMessage( + SENDER, + TARGET, + MESSAGE_VALUE, + messageNonce, + 0, + // this value becomes inaccurate as the oracle price changes + BRIDGE_MESSAGE + ); + + syncPool.sync{value: msgFee.nativeFee}(Constants.ETH_ADDRESS, hex"", msgFee); + } + + /// @notice Test upgrade to native minting functionality and fast/slow sync on L1 + function testNativeMintingL1() public { + // Setup L1 environment + vm.createSelectFork(L1_RPC_URL); + L1NativeMintingScript nativeMintingL1 = new L1NativeMintingScript(); + // contracts have already been deployed hence no need to simulate deployments + // nativeMintingL1.run(); + + // Execute timelock transactions + executeGnosisTransactionBundle("./output/L1NativeMintingScheduleTransactions.json", L1_TIMELOCK_GNOSIS); + vm.warp(block.timestamp + 259200 + 1); // Advance past timelock period + executeGnosisTransactionBundle("./output/L1NativeMintingExecuteTransactions.json", L1_TIMELOCK_GNOSIS); + executeGnosisTransactionBundle("./output/L1NativeMintingSetConfig.json", L1_CONTRACT_CONTROLLER); + + // Test fast-sync scenario + EtherfiL1SyncPoolETH L1syncPool = EtherfiL1SyncPoolETH(L1_SYNC_POOL); + uint256 lockBoxBalanceBefore = IERC20(L1_WEETH).balanceOf(L1syncPool.getLockBox()); + + // Mock inbound LayerZero message from L2SyncPool + // used the data from this call: + // https://layerzeroscan.com/tx/0x1107ae898ad34e942d2e007dbb358c26d24ec578d8e9628fafa9b6c1727ae92d + Origin memory origin = Origin({ + srcEid: SCROLL.L2_EID, + sender: _toBytes32(SCROLL.L2_SYNC_POOL), + nonce: 1 + }); + bytes32 guid = 0x1fb4f4c346dd3904d20a62a68ba66df159e012db8526b776cd5bb07b2f80f20e; + address lzExecutor = 0x173272739Bd7Aa6e4e214714048a9fE699453059; + bytes memory messageL2Message = hex"000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000121cc50fba271ca2860000000000000000000000000000000000000000000000113ae1410d24beb5a6"; + + vm.prank(L1_ENDPOINT); + L1syncPool.lzReceive(origin, guid, messageL2Message, lzExecutor, ""); + + // Verify fast-sync results + IERC20 scrollDummyToken = IERC20(SCROLL.L1_DUMMY_TOKEN); + assertApproxEqAbs(scrollDummyToken.balanceOf(L1_VAMP), 334.114 ether, 0.01 ether); + uint256 lockBoxBalanceAfter = IERC20(L1_WEETH).balanceOf(L1syncPool.getLockBox()); + // As eETH continues to appreciate, the amount received from this fast-sync will decrease from the original 317 weETH + assertApproxEqAbs(lockBoxBalanceAfter, lockBoxBalanceBefore + 317 ether, 1 ether); + + // Test slow-sync scenario + uint256 vampBalanceBefore = L1_VAMP.balance; + + // Mock Scroll messenger call + vm.store( + address(0x6774Bcbd5ceCeF1336b5300fb5186a12DDD8b367), + bytes32(0x00000000000000000000000000000000000000000000000000000000000000c9), + bytes32(uint256(uint160(SENDER))) + ); + + vm.prank(0x6774Bcbd5ceCeF1336b5300fb5186a12DDD8b367); + (bool success, ) = TARGET.call{value: MESSAGE_VALUE}(BRIDGE_MESSAGE); + require(success, "Message call failed"); + + assertEq(vampBalanceBefore + MESSAGE_VALUE, L1_VAMP.balance); + } + +} diff --git a/test/syncSimulation.t.sol b/test/syncSimulation.t.sol index 37f85bb..845cdec 100644 --- a/test/syncSimulation.t.sol +++ b/test/syncSimulation.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "forge-std/Test.sol"; -import "../utils/Constants.sol"; +import "../utils/L2Constants.sol"; interface ILineaBridge { struct ClaimMessageWithProofParams { @@ -24,7 +24,7 @@ interface ILineaBridge { ) external; } -contract simulationLineaClaim is Test, Constants { +contract simulationLineaClaim is Test, L2Constants { address public lineaBridge = 0xd19d4B5d358258f05D7B411E21A1460D11B0876F; @@ -75,7 +75,7 @@ contract simulationLineaClaim is Test, Constants { vm.prank(0xC83bb94779c5577AF1D48dF8e2A113dFf0cB127c); uint256 vampireDummyTokenBalanceBefore = lineaDummyToken.balanceOf(L1_VAMP); - uint256 syncPoolEthBalanceBefore = address(L1_SYNC_POOL_ADDRESS).balance; + uint256 syncPoolEthBalanceBefore = address(L1_SYNC_POOL).balance; uint256 vampireEthBalanceBefore = address(L1_VAMP).balance; console.log("Vampire Linea Dummy Token Balance Before:", vampireDummyTokenBalanceBefore / 1 ether); @@ -85,7 +85,7 @@ contract simulationLineaClaim is Test, Constants { linea.claimMessageWithProof(params); uint256 vampireDummyTokenBalanceAfter = lineaDummyToken.balanceOf(L1_VAMP); - uint256 syncPoolEthBalanceAfter = address(L1_SYNC_POOL_ADDRESS).balance; + uint256 syncPoolEthBalanceAfter = address(L1_SYNC_POOL).balance; uint256 vampireEthBalanceAfter = address(L1_VAMP).balance; console.log("Vampire Linea Dummy Token Balance After:", vampireDummyTokenBalanceAfter / 1 ether); diff --git a/utils/AppendOnlyMerkleTree.sol b/utils/AppendOnlyMerkleTree.sol new file mode 100644 index 0000000..466b5c9 --- /dev/null +++ b/utils/AppendOnlyMerkleTree.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +abstract contract AppendOnlyMerkleTree { + /// @dev The maximum height of the withdraw merkle tree. + uint256 private constant MAX_TREE_HEIGHT = 40; + + /// @notice The merkle root of the current merkle tree. + /// @dev This is actual equal to `branches[n]`. + bytes32 public messageRoot; + + /// @notice The next unused message index. + uint256 public nextMessageIndex; + + /// @notice The list of zero hash in each height. + bytes32[MAX_TREE_HEIGHT] private zeroHashes; + + /// @notice The list of minimum merkle proofs needed to compute next root. + /// @dev Only first `n` elements are used, where `n` is the minimum value that `2^{n-1} >= currentMaxNonce + 1`. + /// It means we only use `currentMaxNonce + 1` leaf nodes to construct the merkle tree. + bytes32[MAX_TREE_HEIGHT] public branches; + + function _initializeMerkleTree() internal { + // Compute hashes in empty sparse Merkle tree + for (uint256 height = 0; height + 1 < MAX_TREE_HEIGHT; height++) { + zeroHashes[height + 1] = _efficientHash(zeroHashes[height], zeroHashes[height]); + } + } + + function _appendMessageHash(bytes32 _messageHash) internal returns (uint256, bytes32) { + require(zeroHashes[1] != bytes32(0), "call before initialization"); + + uint256 _currentMessageIndex = nextMessageIndex; + bytes32 _hash = _messageHash; + uint256 _height = 0; + + while (_currentMessageIndex != 0) { + if (_currentMessageIndex % 2 == 0) { + // it may be used in next round. + branches[_height] = _hash; + // it's a left child, the right child must be null + _hash = _efficientHash(_hash, zeroHashes[_height]); + } else { + // it's a right child, use previously computed hash + _hash = _efficientHash(branches[_height], _hash); + } + unchecked { + _height += 1; + } + _currentMessageIndex >>= 1; + } + + branches[_height] = _hash; + messageRoot = _hash; + + _currentMessageIndex = nextMessageIndex; + unchecked { + nextMessageIndex = _currentMessageIndex + 1; + } + + return (_currentMessageIndex, _hash); + } + + function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { + // solhint-disable-next-line no-inline-assembly + assembly { + mstore(0x00, a) + mstore(0x20, b) + value := keccak256(0x00, 0x40) + } + } +} diff --git a/utils/GnosisHelpers.sol b/utils/GnosisHelpers.sol index 9d3907c..c7b5100 100644 --- a/utils/GnosisHelpers.sol +++ b/utils/GnosisHelpers.sol @@ -26,7 +26,6 @@ contract GnosisHelpers is Test { } } - // Get the gnosis transaction header function _getGnosisHeader(string memory chainId) internal pure returns (string memory) { return string.concat('{"chainId":"', chainId, '","meta": { "txBuilderVersion": "1.16.5" }, "transactions": ['); @@ -65,8 +64,8 @@ contract GnosisHelpers is Test { bytes32 constant salt = 0x0000000000000000000000000000000000000000000000000000000000000000; uint256 constant delay = 259200; - // Generates the schedule transaction for a Timelock - function _getTimelockScheduleTransaction(address to, bytes memory data, bool isLasts) internal pure returns (string memory) { + // Generates the schedule transaction for a gnosis safe + function _getGnosisScheduleTransaction(address to, bytes memory data, bool isLasts) internal pure returns (string memory) { string memory timelockAddressHex = iToHex(abi.encodePacked(address(timelock))); string memory scheduleTransactionData = iToHex(abi.encodeWithSignature("schedule(address,uint256,bytes,bytes32,bytes32,uint256)", to, 0, data, predecessor, salt, delay)); @@ -74,7 +73,7 @@ contract GnosisHelpers is Test { return _getGnosisTransaction(timelockAddressHex, scheduleTransactionData, isLasts); } - function _getTimelockExecuteTransaction(address to, bytes memory data, bool isLasts) internal pure returns (string memory) { + function _getGnosisExecuteTransaction(address to, bytes memory data, bool isLasts) internal pure returns (string memory) { string memory timelockAddressHex = iToHex(abi.encodePacked(address(timelock))); string memory executeTransactionData = iToHex(abi.encodeWithSignature("execute(address,uint256,bytes,bytes32,bytes32)", to, 0, data, predecessor, salt)); diff --git a/utils/Constants.sol b/utils/L2Constants.sol similarity index 94% rename from utils/Constants.sol rename to utils/L2Constants.sol index a75625b..2a8d47b 100644 --- a/utils/Constants.sol +++ b/utils/L2Constants.sol @@ -35,7 +35,6 @@ pragma solidity ^0.8.13; address L2_EXCHANGE_RATE_PROVIDER; address L2_PRICE_ORACLE; address L2_MESSENGER; - uint32 L2_PRICE_ORACLE_HEART_BEAT; address L1_MESSENGER; address L1_DUMMY_TOKEN; @@ -47,10 +46,10 @@ pragma solidity ^0.8.13; address L1_RECEIVER_PROXY_ADMIN; } -contract Constants { +contract L2Constants { /*////////////////////////////////////////////////////////////// - CURRENT DEPLOYMENT CONSTANTS + OFT Deployment Parameters //////////////////////////////////////////////////////////////*/ // General chain constants @@ -79,11 +78,18 @@ contract Constants { // OFT Token Constants string constant TOKEN_NAME = "Wrapped eETH"; string constant TOKEN_SYMBOL = "weETH"; + + // weETH Bridge Rate Limits + uint256 constant BUCKET_SIZE = 3600000000000000000000; + uint256 constant BUCKET_REFILL_PER_SECOND = 1000000000000000000; - // Global Production Rate Limits + // Global Production weETH Bridge Rate Limits uint256 constant LIMIT = 2000 ether; uint256 constant WINDOW = 4 hours; + // Standard Native Minting Rates + uint32 constant L2_PRICE_ORACLE_HEART_BEAT = 24 hours; + // Mainnet Constants string constant L1_RPC_URL = "https://mainnet.gateway.tenderly.co"; uint32 constant L1_EID = 30101; @@ -94,7 +100,7 @@ contract Constants { address constant L1_TIMELOCK_GNOSIS = 0xcdd57D11476c22d265722F68390b036f3DA48c21; address constant L1_TIMELOCK = 0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761; - address constant L1_SYNC_POOL_ADDRESS = 0xD789870beA40D056A4d26055d0bEFcC8755DA146; + address constant L1_SYNC_POOL = 0xD789870beA40D056A4d26055d0bEFcC8755DA146; address constant L1_OFT_ADAPTER = 0xcd2eb13D6831d4602D80E5db9230A57596CDCA63; address constant L1_OFT_ADAPTER_NEW_IMPL = 0xA82cc578927058af14fD84d96a817Dc85Ac4F946; address constant L1_VAMP = 0x9FFDF407cDe9a93c47611799DA23924Af3EF764F; @@ -151,7 +157,6 @@ contract Constants { L2_EXCHANGE_RATE_PROVIDER: 0xc42853c0C6624F42fcB8219aCeb67Ad188087DCB, L2_PRICE_ORACLE: 0xcD96262Df56127f298b452FA40759632868A472a, L2_MESSENGER: 0x4200000000000000000000000000000000000007, - L2_PRICE_ORACLE_HEART_BEAT: 24 hours, L1_MESSENGER: 0x5D4472f31Bd9385709ec61305AFc749F0fA8e9d0, L1_DUMMY_TOKEN: 0x83998e169026136760bE6AF93e776C2F352D4b28, @@ -187,7 +192,6 @@ contract Constants { L2_EXCHANGE_RATE_PROVIDER: 0xc42853c0C6624F42fcB8219aCeb67Ad188087DCB, L2_PRICE_ORACLE: 0x7C1DAAE7BB0688C9bfE3A918A4224041c7177256, L2_MESSENGER: 0xC0d3c0d3c0D3c0D3C0d3C0D3C0D3c0d3c0d30007, - L2_PRICE_ORACLE_HEART_BEAT: 6 hours, L1_MESSENGER: 0x95bDCA6c8EdEB69C98Bd5bd17660BaCef1298A6f, L1_DUMMY_TOKEN: 0xDc400f3da3ea5Df0B7B6C127aE2e54CE55644CF3, @@ -223,7 +227,6 @@ contract Constants { L2_EXCHANGE_RATE_PROVIDER: 0x241a91F095B2020890Bc8518bea168C195518344, L2_PRICE_ORACLE: 0x100c8e61aB3BeA812A42976199Fc3daFbcDD7272, L2_MESSENGER: 0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec, - L2_PRICE_ORACLE_HEART_BEAT: 6 hours, L1_MESSENGER: 0xd19d4B5d358258f05D7B411E21A1460D11B0876F, L1_DUMMY_TOKEN: 0x61Ff310aC15a517A846DA08ac9f9abf2A0f9A2bf, @@ -260,7 +263,6 @@ contract Constants { L2_EXCHANGE_RATE_PROVIDER: 0xF2c5519c634796B73dE90c7Dc27B4fEd560fC3ca, L2_PRICE_ORACLE: 0x35e9D7001819Ea3B39Da906aE6b06A62cfe2c181, L2_MESSENGER: 0x4200000000000000000000000000000000000007, - L2_PRICE_ORACLE_HEART_BEAT: 24 hours, L1_MESSENGER: 0x866E82a600A1414e583f7F13623F1aC5d58b0Afa, L1_DUMMY_TOKEN: 0x0295E0CE709723FB25A28b8f67C54a488BA5aE46, @@ -299,7 +301,6 @@ contract Constants { L2_EXCHANGE_RATE_PROVIDER: address(0), L2_PRICE_ORACLE: address(0), L2_MESSENGER: address(0), - L2_PRICE_ORACLE_HEART_BEAT: 0, L1_MESSENGER: address(0), L1_DUMMY_TOKEN: address(0), @@ -335,7 +336,6 @@ contract Constants { L2_EXCHANGE_RATE_PROVIDER: address(0), L2_PRICE_ORACLE: address(0), L2_MESSENGER: address(0), - L2_PRICE_ORACLE_HEART_BEAT: 0, L1_MESSENGER: address(0), L1_DUMMY_TOKEN: address(0), @@ -350,7 +350,7 @@ contract Constants { ConfigPerL2 SCROLL = ConfigPerL2({ NAME: "scroll", - RPC_URL: "https://scroll-mainnet.public.blastapi.io", + RPC_URL: "https://rpc.scroll.io", CHAIN_ID: "534352", L2_EID: 30214, @@ -367,16 +367,15 @@ contract Constants { L2_CONTRACT_CONTROLLER_SAFE: 0x3cD08f51D0EA86ac93368DE31822117cd70CECA3, L2_OFT_PROXY_ADMIN: 0x99fef08aEF9D6955138B66AD16Ab314DB17878ee, - L2_SYNC_POOL: address(0), - L2_SYNC_POOL_RATE_LIMITER: address(0), - L2_EXCHANGE_RATE_PROVIDER: address(0), - L2_PRICE_ORACLE: address(0), - L2_MESSENGER: address(0), - L2_PRICE_ORACLE_HEART_BEAT: 0, - - L1_MESSENGER: address(0), - L1_DUMMY_TOKEN: address(0), - L1_RECEIVER: address(0), + L2_SYNC_POOL: 0x750cf0fd3bc891D8D864B732BC4AD340096e5e68, + L2_SYNC_POOL_RATE_LIMITER: 0x2Ebb099290C7Fb42Df5A31203fEc47EbEe15d576, + L2_EXCHANGE_RATE_PROVIDER: 0x6233BC9931De25A86be259D51Ca45558bbd6e8A7, + L2_PRICE_ORACLE: 0x57bd9E614f542fB3d6FeF2B744f3B813f0cc1258 , + L2_MESSENGER: 0x781e90f1c8Fc4611c9b7497C3B47F99Ef6969CbC, + + L1_MESSENGER: 0x6774Bcbd5ceCeF1336b5300fb5186a12DDD8b367, + L1_DUMMY_TOKEN: 0x641B33A2e1e46F3af8f3f0F9249e9111F24A51B3, + L1_RECEIVER: 0xb7c7CC336390c26BF6eC810e2f79BccBDb660567, L2_SYNC_POOL_PROXY_ADMIN: address(0), L2_EXCHANGE_RATE_PROVIDER_PROXY_ADMIN: address(0), @@ -408,7 +407,6 @@ contract Constants { L2_EXCHANGE_RATE_PROVIDER: address(0), L2_PRICE_ORACLE: address(0), L2_MESSENGER: address(0), - L2_PRICE_ORACLE_HEART_BEAT: 0, L1_MESSENGER: address(0), L1_DUMMY_TOKEN: address(0), @@ -444,7 +442,6 @@ contract Constants { L2_EXCHANGE_RATE_PROVIDER: address(0), L2_PRICE_ORACLE: address(0), L2_MESSENGER: address(0), - L2_PRICE_ORACLE_HEART_BEAT: 0, L1_MESSENGER: address(0), L1_DUMMY_TOKEN: address(0), diff --git a/utils/LayerZeroHelpers.sol b/utils/LayerZeroHelpers.sol index b25d003..55293d6 100644 --- a/utils/LayerZeroHelpers.sol +++ b/utils/LayerZeroHelpers.sol @@ -2,12 +2,16 @@ pragma solidity ^0.8.0; import "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; +import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol"; +import "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/interfaces/IOAppOptionsType3.sol"; +import "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; import "../contracts/PairwiseRateLimiter.sol"; -import "./Constants.sol"; +import "./L2Constants.sol"; contract LayerZeroHelpers { + using OptionsBuilder for bytes; // TODO: move all layerzero helper functions here // Converts an address to bytes32 @@ -67,4 +71,52 @@ contract LayerZeroHelpers { return abi.encode(ulnConfig); } + function getEnforcedOptions(uint32 _eid) public pure returns (EnforcedOptionParam[] memory) { + EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](3); + + enforcedOptions[0] = EnforcedOptionParam({ + eid: _eid, + msgType: 0, + options: OptionsBuilder.newOptions().addExecutorLzReceiveOption(1_000_000, 0) + }); + + enforcedOptions[1] = EnforcedOptionParam({ + eid: _eid, + msgType: 1, + options: OptionsBuilder.newOptions().addExecutorLzReceiveOption(1_000_000, 0) + }); + enforcedOptions[2] = EnforcedOptionParam({ + eid: _eid, + msgType: 2, + options: OptionsBuilder.newOptions().addExecutorLzReceiveOption(1_000_000, 0) + }); + + return enforcedOptions; + } + + function getDVNConfig(uint32 eid, address[2] memory lzDvn) internal pure returns (SetConfigParam[] memory) { + SetConfigParam[] memory params = new SetConfigParam[](1); + address[] memory requiredDVNs = new address[](2); + if (lzDvn[0] > lzDvn[1]) { + requiredDVNs[0] = lzDvn[1]; + requiredDVNs[1] = lzDvn[0]; + } else { + requiredDVNs[0] = lzDvn[0]; + requiredDVNs[1] = lzDvn[1]; + } + + UlnConfig memory ulnConfig = UlnConfig({ + confirmations: 64, + requiredDVNCount: 2, + optionalDVNCount: 0, + optionalDVNThreshold: 0, + requiredDVNs: requiredDVNs, + optionalDVNs: new address[](0) + }); + + params[0] = SetConfigParam(eid, 2, abi.encode(ulnConfig)); + + return params; + } + }