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/contracts/NativeMinting/BucketRateLimiter.sol b/contracts/NativeMinting/BucketRateLimiter.sol new file mode 100644 index 0000000..b20902b --- /dev/null +++ b/contracts/NativeMinting/BucketRateLimiter.sol @@ -0,0 +1,82 @@ +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import "forge-std/console.sol"; + +import "../../interfaces/IRateLimiter.sol"; +import "../../utils/BucketLimiter.sol"; + +contract BucketRateLimiter is IRateLimiter, Initializable, PausableUpgradeable, OwnableUpgradeable, UUPSUpgradeable { + + BucketLimiter.Limit public limit; + address public consumer; + + mapping(address => bool) public admins; + mapping(address => bool) public pausers; + + event UpdatedAdmin(address indexed admin, bool status); + event UpdatedPauser(address indexed pauser, bool status); + + constructor() { + _disableInitializers(); + } + + function initialize(address owner) external initializer { + __Pausable_init(); + __Ownable_init(owner); + __UUPSUpgradeable_init(); + + limit = BucketLimiter.create(0, 0); + } + + function updateRateLimit(address sender, address tokenIn, uint256 amountIn, uint256 amountOut) external whenNotPaused { + require(msg.sender == consumer, "NOT_CONSUMER"); + // Count both 'amountIn' and 'amountOut' as rate limit consumption + uint64 consumedAmount = SafeCast.toUint64((amountIn + amountOut + 1e12 - 1) / 1e12); + require(BucketLimiter.consume(limit, consumedAmount), "BucketRateLimiter: rate limit exceeded"); + } + + function setCapacity(uint256 capacity) external onlyOwner { + // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough + uint64 capacity64 = SafeCast.toUint64(capacity / 1e12); + BucketLimiter.setCapacity(limit, capacity64); + } + + function setRefillRatePerSecond(uint256 refillRate) external onlyOwner { + // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough + uint64 refillRate64 = SafeCast.toUint64(refillRate / 1e12); + BucketLimiter.setRefillRate(limit, refillRate64); + } + + function updateConsumer(address _consumer) external onlyOwner { + consumer = _consumer; + } + + function updateAdmin(address admin, bool status) external onlyOwner { + admins[admin] = status; + emit UpdatedAdmin(admin, status); + } + + function updatePauser(address pauser, bool status) external onlyOwner { + pausers[pauser] = status; + emit UpdatedPauser(pauser, status); + } + + function pauseContract() external { + require(pausers[msg.sender] || admins[msg.sender] || msg.sender == owner(), "NOT_PAUSER"); + _pause(); + } + + function unPauseContract() external { + require(admins[msg.sender] || msg.sender == owner(), "NOT_ADMIN"); + _unpause(); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + +} diff --git a/contracts/NativeMinting/DummyTokenUpgradeable.sol b/contracts/NativeMinting/DummyTokenUpgradeable.sol new file mode 100644 index 0000000..e3f1e58 --- /dev/null +++ b/contracts/NativeMinting/DummyTokenUpgradeable.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; + +import {IDummyToken} from "../../interfaces/IDummyToken.sol"; + +/** + * @title Dummy Token + * @dev ERC20 token with mint and burn functions. + * This token is expected to be used as an accounting token for anticipated deposits. + * For example, when a user deposit ETH on an L2, it needs ~7 days to be sent back to the L1, + * using a faster bridge such as LayerZero allows to deposit a dummy ETH token on the L1 + * to keep track of the actual ETH amount deposited on the L1 and L2, without any delay. + * The dummy token will then be exchanged against the actual ETH when the ETH withdrawal is completed. + */ +contract DummyTokenUpgradeable is ERC20Upgradeable, AccessControlUpgradeable, IDummyToken { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + uint8 private immutable _decimals; + + /** + * @dev Constructor for DummyToken + * @param decimals_ The number of decimals the token uses + */ + constructor(uint8 decimals_) { + _decimals = decimals_; + _disableInitializers(); + } + + /** + * @dev Initializes the contract + * @param name The name of the token + * @param symbol The symbol of the token + * @param owner The owner of the token + */ + function initialize(string memory name, string memory symbol, address owner) external initializer { + __ERC20_init(name, symbol); + + _grantRole(DEFAULT_ADMIN_ROLE, owner); + } + + /** + * @dev Get the number of decimals the token uses + * @return The number of decimals the token uses + */ + function decimals() public view override returns (uint8) { + return _decimals; + } + + /** + * @dev Mint function that can only be called by a minter + * @param to The account to mint the tokens to + * @param amount The amount of tokens to mint + */ + function mint(address to, uint256 amount) external virtual override onlyRole(MINTER_ROLE) { + _mint(to, amount); + } + + /** + * @dev Burn function that can be called by anyone + * @param amount The amount of tokens to burn + */ + function burn(uint256 amount) external virtual override { + _burn(msg.sender, amount); + } +} diff --git a/contracts/EtherfiL1SyncPoolETH.sol b/contracts/NativeMinting/EtherfiL1SyncPoolETH.sol similarity index 97% rename from contracts/EtherfiL1SyncPoolETH.sol rename to contracts/NativeMinting/EtherfiL1SyncPoolETH.sol index 922c818..08815e7 100644 --- a/contracts/EtherfiL1SyncPoolETH.sol +++ b/contracts/NativeMinting/EtherfiL1SyncPoolETH.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IDummyToken} from "../interfaces/IDummyToken.sol"; +import {IDummyToken} from "../../interfaces/IDummyToken.sol"; import {L1BaseSyncPoolUpgradeable, Constants} from "./L1BaseSyncPoolUpgradeable.sol"; -import {ILiquifier} from "../interfaces/ILiquifier.sol"; -import {IWeEth} from "../interfaces/IWeEth.sol"; +import {ILiquifier} from "../../interfaces/ILiquifier.sol"; +import {IWeEth} from "../../interfaces/IWeEth.sol"; contract EtherfiL1SyncPoolETH is L1BaseSyncPoolUpgradeable { error EtherfiL1SyncPoolETH__OnlyETH(); diff --git a/contracts/NativeMinting/EtherfiL2ExchangeRateProvider.sol b/contracts/NativeMinting/EtherfiL2ExchangeRateProvider.sol new file mode 100644 index 0000000..a4cd1c6 --- /dev/null +++ b/contracts/NativeMinting/EtherfiL2ExchangeRateProvider.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {L2ExchangeRateProviderUpgradeable} from "./L2ExchangeRateProviderUpgradeable.sol"; +import {IAggregatorV3} from "../../interfaces/IAggregatorV3.sol"; + +contract EtherfiL2ExchangeRateProvider is L2ExchangeRateProviderUpgradeable { + error EtherfiL2ExchangeRateProvider__InvalidRate(); + + constructor() { + _disableInitializers(); + } + + function initialize(address owner) external initializer { + __Ownable_init(owner); + } + + /** + * @dev Internal function to get rate and last updated time from a rate oracle + * @param rateOracle Rate oracle contract + * @return rate The exchange rate in 1e18 precision + * @return lastUpdated Last updated time + */ + function _getRateAndLastUpdated(address rateOracle, address) + internal + view + override + returns (uint256 rate, uint256 lastUpdated) + { + (, int256 answer,, uint256 updatedAt,) = IAggregatorV3(rateOracle).latestRoundData(); + + if (answer <= 0) revert EtherfiL2ExchangeRateProvider__InvalidRate(); + + // adjust 'answer' based on Oracle feed's precision to have 1e18 precision + // rate * 1e18 / 10**oracle.decimals() + uint8 oracleDecimals = IAggregatorV3(rateOracle).decimals(); + return (uint256(uint256(answer) * 1e18 / 10**oracleDecimals), updatedAt); + } +} diff --git a/contracts/NativeMinting/L1BaseReceiverUpgradeable.sol b/contracts/NativeMinting/L1BaseReceiverUpgradeable.sol new file mode 100644 index 0000000..5c56ee7 --- /dev/null +++ b/contracts/NativeMinting/L1BaseReceiverUpgradeable.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import {BaseMessengerUpgradeable} from "../../utils/BaseMessengerUpgradeable.sol"; +import {IL1SyncPool} from "../../interfaces/IL1SyncPool.sol"; +import {IL1Receiver} from "../../interfaces/IL1Receiver.sol"; + +/** + * @title L1 Base Receiver + * @notice Base contract for L1 receivers + * This contract is intended to receive messages from the native L2 bridge, decode the message + * and then forward it to the L1 sync pool. + */ +abstract contract L1BaseReceiverUpgradeable is OwnableUpgradeable, BaseMessengerUpgradeable, IL1Receiver { + struct L1BaseReceiverStorage { + IL1SyncPool l1SyncPool; + } + + // keccak256(abi.encode(uint256(keccak256(l1basereceiver.storage.l1syncpool)) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant L1BaseReceiverStorageLocation = + 0xec90cfc37697dc33dbcf188d524bdc2a41f251df5a390991a45d6388ac04b500; + + function _getL1BaseReceiverStorage() internal pure returns (L1BaseReceiverStorage storage $) { + assembly { + $.slot := L1BaseReceiverStorageLocation + } + } + + error L1BaseReceiver__UnauthorizedCaller(); + error L1BaseReceiver__UnauthorizedL2Sender(); + + event L1SyncPoolSet(address l1SyncPool); + + function __L1BaseReceiver_init(address l1SyncPool, address messenger) internal onlyInitializing { + __BaseMessenger_init(messenger); + __L1BaseReceiver_init_unchained(l1SyncPool); + } + + function __L1BaseReceiver_init_unchained(address l1SyncPool) internal onlyInitializing { + _setL1SyncPool(l1SyncPool); + } + + /** + * @dev Get the L1 sync pool address + * @return The L1 sync pool address + */ + function getL1SyncPool() public view virtual returns (address) { + L1BaseReceiverStorage storage $ = _getL1BaseReceiverStorage(); + return address($.l1SyncPool); + } + + /** + * @dev Set the L1 sync pool address + * @param l1SyncPool The L1 sync pool address + */ + function setL1SyncPool(address l1SyncPool) public virtual onlyOwner { + _setL1SyncPool(l1SyncPool); + } + + /** + * @dev Internal function to set the L1 sync pool address + * @param l1SyncPool The L1 sync pool address + */ + function _setL1SyncPool(address l1SyncPool) internal virtual { + L1BaseReceiverStorage storage $ = _getL1BaseReceiverStorage(); + $.l1SyncPool = IL1SyncPool(l1SyncPool); + + emit L1SyncPoolSet(l1SyncPool); + } + + /** + * @dev Internal function to forward the message to the L1 sync pool + * @param originEid Origin endpoint ID + * @param sender Sender address + * @param guid Message GUID + * @param tokenIn Token address + * @param amountIn Amount of tokens + * @param amountOut Amount of tokens + * @param valueToL1SyncPool Value to send to the L1 sync pool + */ + function _forwardToL1SyncPool( + uint32 originEid, + bytes32 sender, + bytes32 guid, + address tokenIn, + uint256 amountIn, + uint256 amountOut, + uint256 valueToL1SyncPool + ) internal virtual { + if (msg.sender != getMessenger()) revert L1BaseReceiver__UnauthorizedCaller(); + if (_getAuthorizedL2Address(originEid) != sender) revert L1BaseReceiver__UnauthorizedL2Sender(); + + L1BaseReceiverStorage storage $ = _getL1BaseReceiverStorage(); + $.l1SyncPool.onMessageReceived{value: valueToL1SyncPool}(originEid, guid, tokenIn, amountIn, amountOut); + } + + /** + * @dev Internal function to get the authorized L2 address + * @param originEid Origin endpoint ID + * @return The authorized L2 address + */ + function _getAuthorizedL2Address(uint32 originEid) internal view virtual returns (bytes32) { + L1BaseReceiverStorage storage $ = _getL1BaseReceiverStorage(); + return $.l1SyncPool.peers(originEid); + } +} diff --git a/contracts/L1BaseSyncPoolUpgradeable.sol b/contracts/NativeMinting/L1BaseSyncPoolUpgradeable.sol similarity index 99% rename from contracts/L1BaseSyncPoolUpgradeable.sol rename to contracts/NativeMinting/L1BaseSyncPoolUpgradeable.sol index ed74769..d942258 100644 --- a/contracts/L1BaseSyncPoolUpgradeable.sol +++ b/contracts/NativeMinting/L1BaseSyncPoolUpgradeable.sol @@ -10,8 +10,8 @@ import { } from "@layerzerolabs/lz-evm-oapp-v2/contracts-upgradeable/oapp/OAppReceiverUpgradeable.sol"; import {Origin} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; -import {IL1SyncPool} from "../interfaces/IL1SyncPool.sol"; -import {Constants} from "../libraries/Constants.sol"; +import {IL1SyncPool} from "../../interfaces/IL1SyncPool.sol"; +import {Constants} from "../../libraries/Constants.sol"; /** * @title L1 Base Sync Pool diff --git a/contracts/NativeMinting/L2BaseSyncPoolUpgradeable.sol b/contracts/NativeMinting/L2BaseSyncPoolUpgradeable.sol new file mode 100644 index 0000000..09cf462 --- /dev/null +++ b/contracts/NativeMinting/L2BaseSyncPoolUpgradeable.sol @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { + OAppSenderUpgradeable, + OAppCoreUpgradeable +} from "@layerzerolabs/lz-evm-oapp-v2/contracts-upgradeable/oapp/OAppSenderUpgradeable.sol"; +import {OAppOptionsType3Upgradeable} from + "@layerzerolabs/lz-evm-oapp-v2/contracts-upgradeable/oapp/libs/OAppOptionsType3Upgradeable.sol"; +import { + MessagingFee, + MessagingReceipt +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import {IL2ExchangeRateProvider} from "../../interfaces/IL2ExchangeRateProvider.sol"; +import {IL2SyncPool} from "../../interfaces/IL2SyncPool.sol"; +import {IMintableERC20} from "../../interfaces/IMintableERC20.sol"; +import {IRateLimiter} from "../../interfaces/IRateLimiter.sol"; +import {Constants} from "../../libraries/Constants.sol"; + +/** + * @title L2 Base Sync Pool + * @dev Base contract for Layer 2 sync pools + * A sync pool is an OApp that allows users to deposit tokens on Layer 2, and then sync them to Layer 1 + * using the LayerZero messaging protocol. + * The L2 sync pool takes care of deposits on the L2 and syncing to the L1 using the L1 sync pool. + * Once enough tokens have been deposited, anyone can trigger a sync to Layer 1. + */ +abstract contract L2BaseSyncPoolUpgradeable is + OAppSenderUpgradeable, + OAppOptionsType3Upgradeable, + ReentrancyGuardUpgradeable, + IL2SyncPool +{ + struct L2BaseSyncPoolStorage { + IL2ExchangeRateProvider l2ExchangeRateProvider; + IRateLimiter rateLimiter; + address tokenOut; + uint32 dstEid; + mapping(address => Token) tokens; + } + + /** + * @dev Token data + * @param unsyncedAmountIn Amount of tokens deposited on Layer 2 + * @param unsyncedAmountOut Amount of tokens minted on Layer 2 + * @param minSyncAmount Minimum amount of tokens required to sync + * @param l1Address Address of the token on Layer 1, address(0) is unauthorized + */ + struct Token { + uint256 unsyncedAmountIn; + uint256 unsyncedAmountOut; + uint256 minSyncAmount; + address l1Address; + } + + // keccak256(abi.encode(uint256(keccak256(syncpools.storage.l2basesyncpool)) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant L2BaseSyncPoolStorageLocation = + 0x4b36603b35af025fe7f5b305ecc1a13c2c1ca8257f1efc0a04a9ab3253595100; + + function _getL2BaseSyncPoolStorage() internal pure returns (L2BaseSyncPoolStorage storage $) { + assembly { + $.slot := L2BaseSyncPoolStorageLocation + } + } + + error L2BaseSyncPool__ZeroAmount(); + error L2BaseSyncPool__InsufficientAmountOut(); + error L2BaseSyncPool__InsufficientAmountToSync(); + error L2BaseSyncPool__UnauthorizedToken(); + error L2BaseSyncPool__InvalidAmountIn(); + + event L2ExchangeRateProviderSet(address l2ExchangeRateProvider); + event RateLimiterSet(address rateLimiter); + event TokenOutSet(address tokenOut); + event DstEidSet(uint32 dstEid); + event MinSyncAmountSet(address tokenIn, uint256 minSyncAmount); + event L1TokenInSet(address tokenIn, address l1TokenIn); + event Deposit(address indexed tokenIn, uint256 amountIn, uint256 amountOut); + event Sync(uint32 dstEid, address indexed tokenIn, uint256 amountIn, uint256 amountOut); + + /** + * @dev Constructor for L2 Base Sync Pool + * @param endpoint Address of the LayerZero endpoint + */ + constructor(address endpoint) OAppCoreUpgradeable(endpoint) {} + + /** + * @dev Initialize the L2 Base Sync Pool + * @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 delegate Address of the delegate + */ + function __L2BaseSyncPool_init( + address l2ExchangeRateProvider, + address rateLimiter, + address tokenOut, + uint32 dstEid, + address delegate + ) internal onlyInitializing { + __ReentrancyGuard_init(); + __OAppCore_init(delegate); + __L2BaseSyncPool_init_unchained(l2ExchangeRateProvider, rateLimiter, tokenOut, dstEid); + } + + function __L2BaseSyncPool_init_unchained( + address l2ExchangeRateProvider, + address rateLimiter, + address tokenOut, + uint32 dstEid + ) internal onlyInitializing { + _setL2ExchangeRateProvider(l2ExchangeRateProvider); + _setRateLimiter(rateLimiter); + _setTokenOut(tokenOut); + _setDstEid(dstEid); + } + + /** + * @dev Get the exchange rate provider + * @return l2ExchangeRateProvider Address of the exchange rate provider + */ + function getL2ExchangeRateProvider() public view virtual returns (address) { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + return address($.l2ExchangeRateProvider); + } + + /** + * @dev Get the rate limiter + * @return rateLimiter Address of the rate limiter + */ + function getRateLimiter() public view virtual returns (address) { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + return address($.rateLimiter); + } + + /** + * @dev Get the token to mint on Layer 2 + * @return tokenOut Address of the token to mint on Layer 2 + */ + function getTokenOut() public view virtual returns (address) { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + return address($.tokenOut); + } + + /** + * @dev Get the destination endpoint ID, most of the time the Layer 1 endpoint ID + * @return dstEid Destination endpoint ID + */ + function getDstEid() public view virtual returns (uint32) { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + return $.dstEid; + } + + /** + * @dev Get token data + * If the l1Address is address(0), the token is unauthorized + * @param tokenIn Address of the token + * @return token Token data + */ + function getTokenData(address tokenIn) public view virtual returns (Token memory) { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + return $.tokens[tokenIn]; + } + + /** + * @dev Quote the messaging fee for a sync + * @param tokenIn Address of the token + * @param extraOptions Extra options for the messaging protocol + * @param payInLzToken Whether to pay the fee in LZ token + * @return msgFee Messaging fee + */ + function quoteSync(address tokenIn, bytes calldata extraOptions, bool payInLzToken) + public + view + virtual + returns (MessagingFee memory msgFee) + { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + + Token storage token = $.tokens[tokenIn]; + uint32 dstEid = $.dstEid; + + (bytes memory message, bytes memory options) = + _buildMessageAndOptions(dstEid, tokenIn, token.unsyncedAmountIn, token.unsyncedAmountOut, extraOptions); + + return _quote(dstEid, message, options, payInLzToken); + } + + /** + * @dev Deposit tokens on Layer 2 + * This will mint tokenOut on Layer 2 using the exchange rate for tokenIn to tokenOut. + * The amount deposited and minted will be stored in the token data which can be synced to Layer 1. + * Will revert if: + * - The amountIn is zero + * - The token is unauthorized (that is, the l1Address is address(0)) + * - The amountOut is less than the minAmountOut + * @param tokenIn Address of the token + * @param amountIn Amount of tokens to deposit + * @param minAmountOut Minimum amount of tokens to mint on Layer 2 + * @return amountOut Amount of tokens minted on Layer 2 + */ + function deposit(address tokenIn, uint256 amountIn, uint256 minAmountOut) + public + payable + virtual + override + nonReentrant + returns (uint256 amountOut) + { + if (amountIn == 0) revert L2BaseSyncPool__ZeroAmount(); + + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + + Token storage token = $.tokens[tokenIn]; + if (token.l1Address == address(0)) revert L2BaseSyncPool__UnauthorizedToken(); + + emit Deposit(tokenIn, amountIn, minAmountOut); + + _receiveTokenIn(tokenIn, amountIn); + + amountOut = $.l2ExchangeRateProvider.getConversionAmount(tokenIn, amountIn); + if (amountOut < minAmountOut) revert L2BaseSyncPool__InsufficientAmountOut(); + + token.unsyncedAmountIn += amountIn; + token.unsyncedAmountOut += amountOut; + + IRateLimiter rateLimiter = $.rateLimiter; + if (address(rateLimiter) != address(0)) rateLimiter.updateRateLimit(msg.sender, tokenIn, amountIn, amountOut); + + _sendTokenOut(msg.sender, amountOut); + + return amountOut; + } + + /** + * @dev Sync tokens to Layer 1 + * This will send a message to the destination endpoint with the token data to + * sync the tokens minted on Layer 2 to Layer 1. + * Will revert if: + * - The token is unauthorized (that is, the l1Address is address(0)) + * - The amount to sync is zero or less than the minSyncAmount + * @dev It is very important to listen for the Sync event to know when and how much tokens were synced + * especially if an action is required on another chain (for example, executing the message). If an action + * was required but was not executed, the tokens won't be sent to the L1. + * @param tokenIn Address of the token + * @param extraOptions Extra options for the messaging protocol + * @param fee Messaging fee + * @return unsyncedAmountIn Amount of tokens deposited on Layer 2 + * @return unsyncedAmountOut Amount of tokens minted on Layer 2 + */ + function sync(address tokenIn, bytes calldata extraOptions, MessagingFee calldata fee) + public + payable + virtual + override + nonReentrant + returns (uint256 unsyncedAmountIn, uint256 unsyncedAmountOut) + { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + Token storage token = $.tokens[tokenIn]; + + address l1TokenIn = token.l1Address; + if (l1TokenIn == address(0)) revert L2BaseSyncPool__UnauthorizedToken(); + + unsyncedAmountIn = token.unsyncedAmountIn; + unsyncedAmountOut = token.unsyncedAmountOut; + + if (unsyncedAmountIn == 0 || unsyncedAmountIn < token.minSyncAmount) { + revert L2BaseSyncPool__InsufficientAmountToSync(); + } + + token.unsyncedAmountIn = 0; + token.unsyncedAmountOut = 0; + + uint32 dstEid = $.dstEid; + + emit Sync(dstEid, tokenIn, unsyncedAmountIn, unsyncedAmountOut); + + _sync(dstEid, tokenIn, l1TokenIn, unsyncedAmountIn, unsyncedAmountOut, extraOptions, fee); + + return (unsyncedAmountIn, unsyncedAmountOut); + } + + /** + * @dev Set the exchange rate provider + * @param l2ExchangeRateProvider Address of the exchange rate provider + */ + function setL2ExchangeRateProvider(address l2ExchangeRateProvider) public virtual onlyOwner { + _setL2ExchangeRateProvider(l2ExchangeRateProvider); + } + + /** + * @dev Set the rate limiter + * @param rateLimiter Address of the rate limiter + */ + function setRateLimiter(address rateLimiter) public virtual onlyOwner { + _setRateLimiter(rateLimiter); + } + + /** + * @dev Set the token to mint on Layer 2 + * @param tokenOut Address of the token to mint on Layer 2 + */ + function setTokenOut(address tokenOut) public virtual onlyOwner { + _setTokenOut(tokenOut); + } + + /** + * @dev Set the destination endpoint ID, most of the time the Layer 1 endpoint ID + * @param dstEid Destination endpoint ID + */ + function setDstEid(uint32 dstEid) public virtual onlyOwner { + _setDstEid(dstEid); + } + + /** + * @dev Set the minimum amount of tokens required to sync + * @param tokenIn Address of the token + * @param minSyncAmount Minimum amount of tokens required to sync + */ + function setMinSyncAmount(address tokenIn, uint256 minSyncAmount) public virtual onlyOwner { + _setMinSyncAmount(tokenIn, minSyncAmount); + } + + /** + * @dev Set the Layer 1 address of the token + * @param l2TokenIn Address of the token on Layer 2 + * @param l1TokenIn Address of the token on Layer 1 + */ + function setL1TokenIn(address l2TokenIn, address l1TokenIn) public virtual onlyOwner { + _setL1TokenIn(l2TokenIn, l1TokenIn); + } + + /** + * @dev Internal function to set the exchange rate provider + * @param l2ExchangeRateProvider Address of the exchange rate provider + */ + function _setL2ExchangeRateProvider(address l2ExchangeRateProvider) internal virtual { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + $.l2ExchangeRateProvider = IL2ExchangeRateProvider(l2ExchangeRateProvider); + + emit L2ExchangeRateProviderSet(l2ExchangeRateProvider); + } + + /** + * @dev Internal function to set the rate limiter + * @param rateLimiter Address of the rate limiter + */ + function _setRateLimiter(address rateLimiter) internal virtual { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + $.rateLimiter = IRateLimiter(rateLimiter); + + emit RateLimiterSet(rateLimiter); + } + + /** + * @dev Internal function to set the token to mint on Layer 2 + * @param tokenOut Address of the token to mint on Layer 2 + */ + function _setTokenOut(address tokenOut) internal virtual { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + $.tokenOut = tokenOut; + + emit TokenOutSet(tokenOut); + } + + /** + * @dev Internal function to set the destination endpoint ID, most of the time the Layer 1 endpoint ID + * @param dstEid Destination endpoint ID + */ + function _setDstEid(uint32 dstEid) internal virtual { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + $.dstEid = dstEid; + + emit DstEidSet(dstEid); + } + + /** + * @dev Internal function to set the minimum amount of tokens required to sync + * @param tokenIn Address of the token + * @param minSyncAmount Minimum amount of tokens required to sync + */ + function _setMinSyncAmount(address tokenIn, uint256 minSyncAmount) internal virtual { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + $.tokens[tokenIn].minSyncAmount = minSyncAmount; + + emit MinSyncAmountSet(tokenIn, minSyncAmount); + } + + /** + * @dev Internal function to set the Layer 1 address of the token + * @param l2TokenIn Address of the token on Layer 2 + * @param l1TokenIn Address of the token on Layer 1 + */ + function _setL1TokenIn(address l2TokenIn, address l1TokenIn) internal virtual { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + $.tokens[l2TokenIn].l1Address = l1TokenIn; + + emit L1TokenInSet(l2TokenIn, l1TokenIn); + } + + /** + * @dev Internal function to receive tokens on Layer 2 + * @param tokenIn Address of the token + * @param amountIn Amount of tokens to receive + */ + function _receiveTokenIn(address tokenIn, uint256 amountIn) internal virtual { + if (tokenIn == Constants.ETH_ADDRESS) { + if (amountIn != msg.value) revert L2BaseSyncPool__InvalidAmountIn(); + } else { + if (msg.value != 0) revert L2BaseSyncPool__InvalidAmountIn(); + + // warning: not safe with transfer tax tokens + SafeERC20.safeTransferFrom(IERC20(tokenIn), msg.sender, address(this), amountIn); + } + } + + /** + * @dev Internal function to sync tokens to Layer 1 + * @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, + address l1TokenIn, + uint256 amountIn, + uint256 amountOut, + bytes calldata extraOptions, + MessagingFee calldata fee + ) internal virtual returns (MessagingReceipt memory) { + (bytes memory message, bytes memory options) = + _buildMessageAndOptions(dstEid, l1TokenIn, amountIn, amountOut, extraOptions); + + return _lzSend(dstEid, message, options, fee, msg.sender); + } + + /** + * @dev Internal function to build the message and options for the messaging protocol + * @param dstEid Destination endpoint ID + * @param tokenIn Address of the token + * @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 + * @return message Message for the messaging protocol + * @return options Options for the messaging protocol + */ + function _buildMessageAndOptions( + uint32 dstEid, + address tokenIn, + uint256 amountIn, + uint256 amountOut, + bytes calldata extraOptions + ) internal view virtual returns (bytes memory message, bytes memory options) { + message = abi.encode(tokenIn, amountIn, amountOut); + options = combineOptions(dstEid, 0, extraOptions); + } + + /** + * @dev Internal function to send tokenOut to an account + * @param account Address of the account + * @param amount Amount of tokens to send + */ + function _sendTokenOut(address account, uint256 amount) internal virtual { + L2BaseSyncPoolStorage storage $ = _getL2BaseSyncPoolStorage(); + IMintableERC20($.tokenOut).mint(account, amount); + } +} diff --git a/contracts/NativeMinting/L2ExchangeRateProviderUpgradeable.sol b/contracts/NativeMinting/L2ExchangeRateProviderUpgradeable.sol new file mode 100644 index 0000000..051163e --- /dev/null +++ b/contracts/NativeMinting/L2ExchangeRateProviderUpgradeable.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import {IL2ExchangeRateProvider} from "../../interfaces/IL2ExchangeRateProvider.sol"; +import {Constants} from "../../libraries/Constants.sol"; + +/** + * @title Exchange Rate Provider + * @dev Provides exchange rate for different tokens against a common quote token + * The rates oracles are expected to all use the same quote token. + * For example, if quote is ETH and token is worth 2 ETH, the rate should be 2e18. + */ +abstract contract L2ExchangeRateProviderUpgradeable is OwnableUpgradeable, IL2ExchangeRateProvider { + struct L2ExchangeRateProviderStorage { + /** + * @dev Mapping of token address to rate parameters + * All rate oracles are expected to return rates with the `18 + decimalsIn - decimalsOut` decimals + */ + mapping(address => RateParameters) rateParameters; + } + + // keccak256(abi.encode(uint256(keccak256(syncpools.storage.l2exchangerateprovider)) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant L2ExchangeRateProviderStorageLocation = + 0xe04a73ceb6eb109286b5315cfafd156065d9e3fbfa5269d3606a1b3095f3ad00; + + function _getL2ExchangeRateProviderStorage() internal pure returns (L2ExchangeRateProviderStorage storage $) { + assembly { + $.slot := L2ExchangeRateProviderStorageLocation + } + } + + error L2ExchangeRateProvider__DepositFeeExceedsMax(); + error L2ExchangeRateProvider__OutdatedRate(); + error L2ExchangeRateProvider__NoRateOracle(); + + event RateParametersSet(address token, address rateOracle, uint64 depositFee, uint32 freshPeriod); + + function __L2ExchangeRateProvider_init() internal onlyInitializing {} + + function __L2ExchangeRateProvider_init_unchained() internal onlyInitializing {} + + /** + * @dev Get rate parameters for a token + * @param token Token address + * @return parameters Rate parameters + */ + function getRateParameters(address token) public view virtual returns (RateParameters memory parameters) { + L2ExchangeRateProviderStorage storage $ = _getL2ExchangeRateProviderStorage(); + return $.rateParameters[token]; + } + + /** + * @dev Get conversion amount for a token, given an amount in of token it should return the amount out. + * It also applies the deposit fee. + * Will revert if: + * - No rate oracle is set for the token + * - The rate is outdated (fresh period has passed) + * @param token Token address + * @param amountIn Amount in + * @return amountOut Amount out + */ + function getConversionAmount(address token, uint256 amountIn) + public + view + virtual + override + returns (uint256 amountOut) + { + L2ExchangeRateProviderStorage storage $ = _getL2ExchangeRateProviderStorage(); + RateParameters storage rateParameters = $.rateParameters[token]; + + address rateOracle = rateParameters.rateOracle; + + if (rateOracle == address(0)) revert L2ExchangeRateProvider__NoRateOracle(); + + (uint256 rate, uint256 lastUpdated) = _getRateAndLastUpdated(rateOracle, token); + + if (lastUpdated + rateParameters.freshPeriod < block.timestamp) revert L2ExchangeRateProvider__OutdatedRate(); + + uint256 feeAmount = (amountIn * rateParameters.depositFee + Constants.PRECISION_SUB_ONE) / Constants.PRECISION; + uint256 amountInAfterFee = amountIn - feeAmount; + + amountOut = amountInAfterFee * Constants.PRECISION / rate; + + return amountOut; + } + + // Skip the test for freshPeriod requirement + function getConversionAmountUnsafe(address token, uint256 amountIn) + public + view + returns (uint256 amountOut) + { + L2ExchangeRateProviderStorage storage $ = _getL2ExchangeRateProviderStorage(); + RateParameters storage rateParameters = $.rateParameters[token]; + + address rateOracle = rateParameters.rateOracle; + + if (rateOracle == address(0)) revert L2ExchangeRateProvider__NoRateOracle(); + + (uint256 rate, uint256 lastUpdated) = _getRateAndLastUpdated(rateOracle, token); + + uint256 feeAmount = (amountIn * rateParameters.depositFee + Constants.PRECISION_SUB_ONE) / Constants.PRECISION; + uint256 amountInAfterFee = amountIn - feeAmount; + + amountOut = amountInAfterFee * Constants.PRECISION / rate; + + return amountOut; + } + + /** + * @dev Set rate parameters for a token + * @param token Token address + * @param rateOracle Rate oracle contract, providing the exchange rate + * @param depositFee Deposit fee, in 1e18 precision (e.g. 1e16 for 1% fee) + * @param freshPeriod Fresh period, in seconds + */ + function setRateParameters(address token, address rateOracle, uint64 depositFee, uint32 freshPeriod) + public + virtual + onlyOwner + { + _setRateParameters(token, rateOracle, depositFee, freshPeriod); + } + + /** + * @dev Internal function to set rate parameters for a token + * Will revert if: + * - Deposit fee exceeds 100% (1e18) + * @param token Token address + * @param rateOracle Rate oracle contract, providing the exchange rate + * @param depositFee Deposit fee, in 1e18 precision (e.g. 1e16 for 1% fee) + * @param freshPeriod Fresh period, in seconds + */ + function _setRateParameters(address token, address rateOracle, uint64 depositFee, uint32 freshPeriod) + internal + virtual + { + if (depositFee > Constants.PRECISION) revert L2ExchangeRateProvider__DepositFeeExceedsMax(); + + L2ExchangeRateProviderStorage storage $ = _getL2ExchangeRateProviderStorage(); + $.rateParameters[token] = RateParameters(rateOracle, depositFee, freshPeriod); + + emit RateParametersSet(token, rateOracle, depositFee, freshPeriod); + } + + /** + * @dev Internal function to get rate and last updated time from a rate oracle + * @param rateOracle Rate oracle contract + * @param token The token address which the rate is for + * @return rate The exchange rate in 1e18 precision + * @return lastUpdated Last updated time + */ + function _getRateAndLastUpdated(address rateOracle, address token) + internal + view + virtual + returns (uint256 rate, uint256 lastUpdated); +} diff --git a/contracts/NativeMinting/L2SyncPoolContracts/L2OPStackSyncPoolETHUpgradeable.sol b/contracts/NativeMinting/L2SyncPoolContracts/L2OPStackSyncPoolETHUpgradeable.sol new file mode 100644 index 0000000..42e0ac7 --- /dev/null +++ b/contracts/NativeMinting/L2SyncPoolContracts/L2OPStackSyncPoolETHUpgradeable.sol @@ -0,0 +1,124 @@ +// 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 "../../../utils/BaseMessengerUpgradeable.sol"; +import {BaseReceiverUpgradeable} from "../../../utils/BaseReceiverUpgradeable.sol"; +import {L2BaseSyncPoolUpgradeable} from "../L2BaseSyncPoolUpgradeable.sol"; +import {ICrossDomainMessenger} from "../../../interfaces/ICrossDomainMessenger.sol"; +import {Constants} from "../../../libraries/Constants.sol"; +import {IL1Receiver} from "../../../interfaces/IL1Receiver.sol"; + +/** + * @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 L2OPStackSyncPoolETHUpgradeable is L2BaseSyncPoolUpgradeable, BaseMessengerUpgradeable, BaseReceiverUpgradeable { + + event DepositWithReferral(address indexed sender, uint256 amount, address referral); + + error L2OPStackSyncPoolETH__OnlyETH(); + + /** + * @dev Constructor for L2 OP 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 L2OPStackSyncPoolETH__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 L2OPStackSyncPoolETH__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); + + ICrossDomainMessenger(messenger).sendMessage{value: amountIn}(receiver, 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/L2SyncPoolContracts/L2ScrollSyncPoolETHUpgradeable.sol b/contracts/NativeMinting/L2SyncPoolContracts/L2ScrollSyncPoolETHUpgradeable.sol new file mode 100644 index 0000000..ba72b65 --- /dev/null +++ b/contracts/NativeMinting/L2SyncPoolContracts/L2ScrollSyncPoolETHUpgradeable.sol @@ -0,0 +1,125 @@ +// 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 "../../../utils/BaseMessengerUpgradeable.sol"; +import {BaseReceiverUpgradeable} from "../../../utils/BaseReceiverUpgradeable.sol"; +import {L2BaseSyncPoolUpgradeable} from "../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..53ef66b --- /dev/null +++ b/contracts/NativeMinting/ReceiverContracts/L1ScrollReceiverETHUpgradeable.sol @@ -0,0 +1,49 @@ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {L1BaseReceiverUpgradeable} from "../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 f9a2b42..3b9ab1d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,14 +1,12 @@ [profile.default] src = "contracts" -evm_version = "shanghai" out = "out" libs = ["node_modules", "lib"] test = "test" fs_permissions = [{ access = "read-write", path = "./"}] -optimizer = true -optimizer_runs = 200 -solc_version = "0.8.20" - +evm_version = "shanghai" +solc_version = "0.8.26" +via_ir = true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/interfaces/IAggregatorV3.sol b/interfaces/IAggregatorV3.sol new file mode 100644 index 0000000..e764841 --- /dev/null +++ b/interfaces/IAggregatorV3.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IAggregatorV3 { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData(uint80 _roundId) + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} diff --git a/interfaces/ICrossDomainMessenger.sol b/interfaces/ICrossDomainMessenger.sol new file mode 100644 index 0000000..d06764f --- /dev/null +++ b/interfaces/ICrossDomainMessenger.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface ICrossDomainMessenger { + function MESSAGE_VERSION() external view returns (uint16); + function MIN_GAS_CALLDATA_OVERHEAD() external view returns (uint64); + function MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR() external view returns (uint64); + function MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR() external view returns (uint64); + function OTHER_MESSENGER() external view returns (address); + function RELAY_CALL_OVERHEAD() external view returns (uint64); + function RELAY_CONSTANT_OVERHEAD() external view returns (uint64); + function RELAY_GAS_CHECK_BUFFER() external view returns (uint64); + function RELAY_RESERVED_GAS() external view returns (uint64); + function baseGas(bytes memory _message, uint32 _minGasLimit) external pure returns (uint64); + function failedMessages(bytes32) external view returns (bool); + function messageNonce() external view returns (uint256); + function relayMessage( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes memory _message + ) external payable; + function sendMessage(address _target, bytes memory _message, uint32 _minGasLimit) external payable; + function successfulMessages(bytes32) external view returns (bool); + function xDomainMessageSender() external view returns (address); +} diff --git a/interfaces/IL1Receiver.sol b/interfaces/IL1Receiver.sol new file mode 100644 index 0000000..3b6e805 --- /dev/null +++ b/interfaces/IL1Receiver.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IL1Receiver { + function onMessageReceived(bytes calldata message) external payable; +} diff --git a/interfaces/IL1ScrollMessenger.sol b/interfaces/IL1ScrollMessenger.sol new file mode 100644 index 0000000..aa2582e --- /dev/null +++ b/interfaces/IL1ScrollMessenger.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +import {IScrollMessenger} from "./IScrollMessenger.sol"; + +interface IL1ScrollMessenger is IScrollMessenger { + /********** + * Events * + **********/ + + /// @notice Emitted when the maximum number of times each message can be replayed is updated. + /// @param oldMaxReplayTimes The old maximum number of times each message can be replayed. + /// @param newMaxReplayTimes The new maximum number of times each message can be replayed. + event UpdateMaxReplayTimes(uint256 oldMaxReplayTimes, uint256 newMaxReplayTimes); + + /*********** + * Structs * + ***********/ + + struct L2MessageProof { + // The index of the batch where the message belongs to. + uint256 batchIndex; + // Concatenation of merkle proof for withdraw merkle trie. + bytes merkleProof; + } + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @notice Relay a L2 => L1 message with message proof. + /// @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. + /// @param proof The proof used to verify the correctness of the transaction. + function relayMessageWithProof( + address from, + address to, + uint256 value, + uint256 nonce, + bytes memory message, + L2MessageProof memory proof + ) external; + + /// @notice Replay an existing message. + /// @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 messageNonce The nonce for the message to replay. + /// @param message The content of the message. + /// @param newGasLimit New gas limit to be used for this message. + /// @param refundAddress The address of account who will receive the refunded fee. + function replayMessage( + address from, + address to, + uint256 value, + uint256 messageNonce, + bytes memory message, + uint32 newGasLimit, + address refundAddress + ) external payable; + + /// @notice Drop a skipped message. + /// @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 messageNonce The nonce for the message to drop. + /// @param message The content of the message. + function dropMessage( + address from, + address to, + uint256 value, + uint256 messageNonce, + bytes memory message + ) external; +} diff --git a/interfaces/IL2ExchangeRateProvider.sol b/interfaces/IL2ExchangeRateProvider.sol new file mode 100644 index 0000000..aa7d8e4 --- /dev/null +++ b/interfaces/IL2ExchangeRateProvider.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IL2ExchangeRateProvider { + /** + * @dev Rate parameters for a token + * @param rateOracle Rate oracle contract, providing the exchange rate + * @param depositFee Deposit fee, in 1e18 precision (e.g. 1e16 for 1% fee) + * @param freshPeriod Fresh period, in seconds + */ + struct RateParameters { + address rateOracle; + uint64 depositFee; + uint32 freshPeriod; + } + + function getConversionAmount(address tokenIn, uint256 amountIn) external view returns (uint256 amountOut); + function getConversionAmountUnsafe(address token, uint256 amountIn) external view returns (uint256 amountOut); + function getRateParameters(address token) external view returns (RateParameters memory parameters); + function setRateParameters(address token, address rateOracle, uint64 depositFee, uint32 freshPeriod) external; +} 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/interfaces/IL2SyncPool.sol b/interfaces/IL2SyncPool.sol new file mode 100644 index 0000000..c4baa26 --- /dev/null +++ b/interfaces/IL2SyncPool.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IOAppCore} from "@layerzerolabs/lz-evm-oapp-v2/contracts-upgradeable/oapp/interfaces/IOAppCore.sol"; +import {MessagingFee} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +interface IL2SyncPool is IOAppCore { + function deposit(address tokenIn, uint256 amountIn, uint256 minAmountOut) + external + payable + returns (uint256 amountOut); + + function sync(address tokenIn, bytes calldata extraOptions, MessagingFee calldata fee) + external + payable + returns (uint256 unsyncedAmountIn, uint256 unsyncedAmountOut); +} diff --git a/interfaces/IRateLimiter.sol b/interfaces/IRateLimiter.sol new file mode 100644 index 0000000..be3acd6 --- /dev/null +++ b/interfaces/IRateLimiter.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +// Used by {L1,L2}SyncPool +interface IRateLimiter { + function updateRateLimit(address sender, address tokenIn, uint256 amountIn, uint256 amountOut) external; +} diff --git a/interfaces/IScrollMessenger.sol b/interfaces/IScrollMessenger.sol new file mode 100644 index 0000000..82ce7a7 --- /dev/null +++ b/interfaces/IScrollMessenger.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +interface IScrollMessenger { + /********** + * Events * + **********/ + + /// @notice Emitted when a cross domain message is sent. + /// @param sender The address of the sender who initiates the message. + /// @param target The address of target contract to call. + /// @param value The amount of value passed to the target contract. + /// @param messageNonce The nonce of the message. + /// @param gasLimit The optional gas limit passed to L1 or L2. + /// @param message The calldata passed to the target contract. + event SentMessage( + address indexed sender, + address indexed target, + uint256 value, + uint256 messageNonce, + uint256 gasLimit, + bytes message + ); + + /// @notice Emitted when a cross domain message is relayed successfully. + /// @param messageHash The hash of the message. + event RelayedMessage(bytes32 indexed messageHash); + + /// @notice Emitted when a cross domain message is failed to relay. + /// @param messageHash The hash of the message. + event FailedRelayedMessage(bytes32 indexed messageHash); + + /********** + * Errors * + **********/ + + /// @dev Thrown when the given address is `address(0)`. + error ErrorZeroAddress(); + + /************************* + * Public View Functions * + *************************/ + + /// @notice Return the sender of a cross domain message. + function xDomainMessageSender() external view returns (address); + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @notice Send cross chain message from L1 to L2 or L2 to L1. + /// @param target The address of account who receive the message. + /// @param value The amount of ether passed when call target contract. + /// @param message The content of the message. + /// @param gasLimit Gas limit required to complete the message relay on corresponding chain. + function sendMessage( + address target, + uint256 value, + bytes calldata message, + uint256 gasLimit + ) external payable; + + /// @notice Send cross chain message from L1 to L2 or L2 to L1. + /// @param target The address of account who receive the message. + /// @param value The amount of ether passed when call target contract. + /// @param message The content of the message. + /// @param gasLimit Gas limit required to complete the message relay on corresponding chain. + /// @param refundAddress The address of account who will receive the refunded fee. + function sendMessage( + address target, + uint256 value, + bytes calldata message, + uint256 gasLimit, + address refundAddress + ) external payable; +} diff --git a/libraries/AppendOnlyMerkleTree.sol b/libraries/AppendOnlyMerkleTree.sol new file mode 100644 index 0000000..466b5c9 --- /dev/null +++ b/libraries/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/scripts/AdapterMigration/01_DeployUpgradeableAdapter.s.sol b/scripts/AdapterMigration/01_DeployUpgradeableAdapter.s.sol index 5dff005..eb40efd 100644 --- a/scripts/AdapterMigration/01_DeployUpgradeableAdapter.s.sol +++ b/scripts/AdapterMigration/01_DeployUpgradeableAdapter.s.sol @@ -9,13 +9,19 @@ 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"; +<<<<<<< HEAD +import "../../contracts/EtherFiOFTAdapterUpgradeable.sol"; +import "../../utils/L2Constants.sol"; +======= import "../../contracts/EtherfiOFTAdapterUpgradeable.sol"; import "../../utils/Constants.sol"; +>>>>>>> master 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 +66,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 6ccbb64..0ac6968 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/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..e13fd10 --- /dev/null +++ b/scripts/NativeMintingDeployment/DeployConfigureL1.s.sol @@ -0,0 +1,97 @@ +// 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 61e7131..5ceb64e 100644 --- a/scripts/OFTDeployment/01_OFTConfigure.s.sol +++ b/scripts/OFTDeployment/01_OFTConfigure.s.sol @@ -13,7 +13,7 @@ import "@layerzerolabs/lz-evm-oapp-v2/contracts-upgradeable/oapp/interfaces/IOAp import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol"; import "../../contracts/MintableOFTUpgradeable.sol"; -import "../../utils/Constants.sol"; +import "../../utils/L2Constants.sol"; import "../../utils/LayerZeroHelpers.sol"; struct OFTDeployment { @@ -23,7 +23,7 @@ struct OFTDeployment { MintableOFTUpgradeable tokenContract; } -contract DeployOFTScript is Script, Constants, LayerZeroHelpers { +contract DeployOFTScript is Script, L2Constants, LayerZeroHelpers { using OptionsBuilder for bytes; address scriptDeployer; diff --git a/scripts/OFTDeployment/02_UpdateOFTPeersTransactions.s.sol b/scripts/OFTDeployment/02_UpdateOFTPeersTransactions.s.sol index 2a7327f..8114411 100644 --- a/scripts/OFTDeployment/02_UpdateOFTPeersTransactions.s.sol +++ b/scripts/OFTDeployment/02_UpdateOFTPeersTransactions.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 UpdateOFTPeersTransactions is Script, Constants, LayerZeroHelpers { diff --git a/scripts/OFTDeployment/03_OFTOwnershipTransfer.s.sol b/scripts/OFTDeployment/03_OFTOwnershipTransfer.s.sol index d0a93ca..cf2631d 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/MintableOFTUpgradeable.sol"; -import "../../utils/Constants.sol"; +import "../../utils/L2Constants.sol"; contract OFTOwnershipTransfer is Script, Constants { using OptionsBuilder for bytes; diff --git a/scripts/OFTDeployment/04_OFTSend.s.sol b/scripts/OFTDeployment/04_OFTSend.s.sol index c939340..28544bc 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/MintableOFTUpgradeable.sol"; import "forge-std/Test.sol"; -import "../../utils/Constants.sol"; +import "../../utils/L2Constants.sol"; import "../../utils/LayerZeroHelpers.sol"; contract CrossChainSend is Script, Constants, LayerZeroHelpers { diff --git a/scripts/OFTDeployment/05_ProdRateLimit.sol b/scripts/OFTDeployment/05_ProdRateLimit.sol index fedc378..eea0b24 100644 --- a/scripts/OFTDeployment/05_ProdRateLimit.sol +++ b/scripts/OFTDeployment/05_ProdRateLimit.sol @@ -6,7 +6,7 @@ import "forge-std/Script.sol"; 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 SetRateLimits is Script, Constants, LayerZeroHelpers { diff --git a/test/AdapterMigration.t.sol b/test/AdapterMigration.t.sol index 0ebf13c..07f70c8 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/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 473a0a5..5725c0b 100644 --- a/test/OFTDeployment.t.sol +++ b/test/OFTDeployment.t.sol @@ -8,19 +8,19 @@ 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-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"; import { SendParam } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol"; import "../contracts/MintableOFTUpgradeable.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..f6b797b --- /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 "../libraries/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/BaseMessengerUpgradeable.sol b/utils/BaseMessengerUpgradeable.sol new file mode 100644 index 0000000..28eb2fb --- /dev/null +++ b/utils/BaseMessengerUpgradeable.sol @@ -0,0 +1,63 @@ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/** + * @title Base Messenger + * @dev Base contract for setting the messenger contract + */ +abstract contract BaseMessengerUpgradeable is OwnableUpgradeable { + struct BaseMessengerStorage { + address messenger; + } + + // keccak256(abi.encode(uint256(keccak256(syncpools.storage.basemessenger)) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant BaseMessengerStorageLocation = + 0x2d365d82646798ae645c4baa2dc2ee228626f61d8b5395bf298ba125a3c6b100; + + function _getBaseMessengerStorage() internal pure returns (BaseMessengerStorage storage $) { + assembly { + $.slot := BaseMessengerStorageLocation + } + } + + event MessengerSet(address messenger); + + function __BaseMessenger_init(address messenger) internal onlyInitializing { + __BaseMessenger_init_unchained(messenger); + } + + function __BaseMessenger_init_unchained(address messenger) internal onlyInitializing { + _setMessenger(messenger); + } + + /** + * @dev Get the messenger address + * @return The messenger address + */ + function getMessenger() public view virtual returns (address) { + BaseMessengerStorage storage $ = _getBaseMessengerStorage(); + return $.messenger; + } + + /** + * @dev Set the messenger address + * @param messenger The messenger address + */ + function setMessenger(address messenger) public virtual onlyOwner { + _setMessenger(messenger); + } + + /** + * @dev Internal function to set the messenger address + * @param messenger The messenger address + */ + function _setMessenger(address messenger) internal { + BaseMessengerStorage storage $ = _getBaseMessengerStorage(); + $.messenger = messenger; + + emit MessengerSet(messenger); + } +} diff --git a/utils/BaseReceiverUpgradeable.sol b/utils/BaseReceiverUpgradeable.sol new file mode 100644 index 0000000..b43a104 --- /dev/null +++ b/utils/BaseReceiverUpgradeable.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/** + * @title Base Receiver + * @dev Base contract for setting the receiver contract + */ +abstract contract BaseReceiverUpgradeable is OwnableUpgradeable { + struct BaseReceiverStorage { + address receiver; + } + + // keccak256(abi.encode(uint256(keccak256(syncpools.storage.basereceiver)) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant BaseReceiverStorageLocation = + 0x487698e326934c06370ca3c28e3bca79fe27d578048e9d42af7fa98f2e481e00; + + function _getBaseReceiverStorage() internal pure returns (BaseReceiverStorage storage $) { + assembly { + $.slot := BaseReceiverStorageLocation + } + } + + event ReceiverSet(address receiver); + + function __BaseReceiver_init(address receiver) internal onlyInitializing { + __BaseReceiver_init_unchained(receiver); + } + + function __BaseReceiver_init_unchained(address receiver) internal onlyInitializing { + _setReceiver(receiver); + } + + /** + * @dev Get the receiver address + * @return The receiver address + */ + function getReceiver() public view virtual returns (address) { + BaseReceiverStorage storage $ = _getBaseReceiverStorage(); + return $.receiver; + } + + /** + * @dev Set the receiver address + * @param receiver The receiver address + */ + function setReceiver(address receiver) public virtual onlyOwner { + _setReceiver(receiver); + } + + /** + * @dev Internal function to set the receiver address + * @param receiver The receiver address + */ + function _setReceiver(address receiver) internal { + BaseReceiverStorage storage $ = _getBaseReceiverStorage(); + $.receiver = receiver; + + emit ReceiverSet(receiver); + } +} diff --git a/utils/BucketLimiter.sol b/utils/BucketLimiter.sol new file mode 100644 index 0000000..2f9e41d --- /dev/null +++ b/utils/BucketLimiter.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * The BucketLimiter contract is used to limit the rate of some action. + * + * Buckets refill at a constant rate, and have a maximum capacity. Each time + * the consume function is called, the bucket gets depleted by the provided + * amount. If the bucket is empty, the consume function will return false + * and the bucket will not be depleted. Rates are measured in units per + * second. + * + * To limit storage usage to a single slot, the Bucket struct is packed into + * a single word, meaning all fields are uint64. + * + * Examples: + * + * ```sol + * BucketLimiter.Limit storage limit = BucketLimiter.create(100, 1); + * limit.consume(10); // returns true, remaining = 90 + * limit.consume(80); // returns true, remaining = 10 + * limit.consume(20); // returns false, remaining = 10 + * // Wait 10 seconds (10 tokens get refilled) + * limit.consume(20); // returns true, remaining = 0) + * // Increase capacity + * limit.setCapacity(200); // remaining = 0, capacity = 200 + * // Increase refill rate + * limit.setRefillRate(2); // remaining = 0, capacity = 200, refillRate = 2 + * // Wait 10 seconds (20 tokens get refilled) + * limit.consume(20); // returns true, remaining = 0 + * ``` + * + * Developers should notice that rate-limits are vulnerable to two attacks: + * 1. Sybil-attacks: Rate limits should typically be global across all user + * accounts, otherwise an attacker can simply create many accounts to + * bypass the rate limit. + * 2. DoS attacks: Rate limits should typically apply to actions with a + * friction such as a fee or a minimum stake time. Otherwise, an + * attacker can simply spam the action to deplete the rate limit. + */ +library BucketLimiter { + struct Limit { + // The maximum capacity of the bucket, in consumable units (eg. tokens) + uint64 capacity; + // The remaining capacity in the bucket, that can be consumed + uint64 remaining; + // The timestamp of the last time the bucket was refilled + uint64 lastRefill; + // The rate at which the bucket refills, in units per second + uint64 refillRate; + } + + /* + * Creates a new bucket with the given capacity and refill rate. + * + * @param capacity The maximum capacity of the bucket, in consumable units (eg. tokens) + * @param refillRate The rate at which the bucket refills, in units per second + * @return The created bucket + */ + function create(uint64 capacity, uint64 refillRate) internal view returns (Limit memory) { + return Limit({ + capacity: capacity, + remaining: capacity, + lastRefill: uint64(block.timestamp), + refillRate: refillRate + }); + } + + /* + * Consumes the given amount from the bucket, if there is sufficient capacity, and returns + * whether the bucket had enough remaining capacity to consume the amount. + * + * @param limit The bucket to consume from + * @param amount The amount to consume + * @return True if the bucket had enough remaining capacity to consume the amount, false otherwise + */ + function consume(Limit storage limit, uint64 amount) internal returns (bool) { + Limit memory _limit = limit; + _refill(_limit); + if (_limit.remaining < amount) { + return false; + } + limit.remaining = _limit.remaining - amount; + limit.lastRefill = _limit.lastRefill; + return true; + } + + /* + * Refills the bucket based on the time elapsed since the last refill. This effectively simulates + * the idea of the bucket continuously refilling at a constant rate. + * + * @param limit The bucket to refill + */ + function refill(Limit storage limit) internal { + Limit memory _limit = limit; + _refill(_limit); + limit.remaining = _limit.remaining; + limit.lastRefill = _limit.lastRefill; + } + + function _refill(Limit memory limit) internal view { + // We allow for overflow here, as the delta is resilient against it. + uint64 now_ = uint64(block.timestamp); + uint64 delta; + unchecked { + delta = now_ - limit.lastRefill; + } + uint64 tokens = delta * limit.refillRate; + uint64 newRemaining = limit.remaining + tokens; + if (newRemaining > limit.capacity) { + limit.remaining = limit.capacity; + } else { + limit.remaining = newRemaining; + } + limit.lastRefill = now_; + } + + /* + * Sets the capacity of the bucket. If the new capacity is less than the remaining capacity, + * the remaining capacity is set to the new capacity. + * + * @param limit The bucket to set the capacity of + * @param capacity The new capacity + */ + function setCapacity(Limit storage limit, uint64 capacity) internal { + refill(limit); + limit.capacity = capacity; + if (limit.remaining > capacity) { + limit.remaining = capacity; + } + } + + /* + * Sets the refill rate of the bucket, in units per second. + * + * @param limit The bucket to set the refill rate of + * @param refillRate The new refill rate + */ + function setRefillRate(Limit storage limit, uint64 refillRate) internal { + refill(limit); + limit.refillRate = refillRate; + } + + /* + * Sets the remaining capacity of the bucket. If the new remaining capacity is greater than + * the capacity, the remaining capacity is set to the capacity. + * + * @param limit The bucket to set the remaining capacity of + * @param remaining The new remaining capacity + */ + function setRemaining(Limit storage limit, uint64 remaining) internal { + refill(limit); + limit.remaining = remaining; + } +} 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 92% rename from utils/Constants.sol rename to utils/L2Constants.sol index b5a3cd6..fdeac0d 100644 --- a/utils/Constants.sol +++ b/utils/L2Constants.sol @@ -31,7 +31,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; @@ -43,10 +42,10 @@ pragma solidity ^0.8.13; address L1_RECEIVER_PROXY_ADMIN; } -contract Constants { +contract L2Constants { /*////////////////////////////////////////////////////////////// - CURRENT DEPLOYMENT CONSTANTS + OFT Deployment Parameters //////////////////////////////////////////////////////////////*/ // General chain constants @@ -70,19 +69,26 @@ contract Constants { //////////////////////////////////////////////////////////////*/ - address constant DEPLOYER_ADDRESS = 0xaFa61D537A1814DE82776BF600cb10Ff26342208; + address constant DEPLOYER_ADDRESS = 0x8D5AAc5d3d5cda4c404fA7ee31B0822B648Bb150; // 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; - // Global Stand by Rate Limits + // Global Stand By weETH Bridge Rate Limits uint256 constant STANDBY_LIMIT = 0.0001 ether; uint256 constant STANDBY_WINDOW = 1 minutes; + // 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; @@ -93,7 +99,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; @@ -145,7 +151,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, @@ -177,7 +182,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, @@ -209,7 +213,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, @@ -242,7 +245,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, @@ -277,7 +279,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), @@ -309,7 +310,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), @@ -324,7 +324,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, @@ -337,16 +337,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), @@ -374,7 +373,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 c7c619b..8e39489 100644 --- a/utils/LayerZeroHelpers.sol +++ b/utils/LayerZeroHelpers.sol @@ -3,10 +3,14 @@ pragma solidity ^0.8.0; import "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/utils/RateLimiter.sol"; 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 "./Constants.sol"; +import "./L2Constants.sol"; contract LayerZeroHelpers { + using OptionsBuilder for bytes; // TODO: move all layerzero helper functions here // Converts an address to bytes32 @@ -66,4 +70,53 @@ 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; + } + + }