Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions deploy/L2UpdateOFTRateLimitsSpell.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.22;

struct RateLimitConfig {
uint32 eid;
uint48 window;
uint256 limit;
}

interface OFTAdapterLike {
function setRateLimits(RateLimitConfig[] calldata inbound, RateLimitConfig[] calldata outbound) external;
function peers(uint32 eid) external view returns (bytes32);
}

/**
* @title L2UpdateOFTRateLimitsSpell
* @notice L2 spell for updating the rate limits of an OFT adapter.
* Deployed once per L2, and it will be delegate called by L2GovernanceRelay when bridged from core spell.
*/
contract L2UpdateOFTRateLimitsSpell {

function execute(
uint32 dstEid,
address oftAdapter,
uint48 inboundWindow,
uint256 inboundLimit,
uint48 outboundWindow,
uint256 outboundLimit
) external {
OFTAdapterLike oft = OFTAdapterLike(oftAdapter);

// Sanity check
require(oft.peers(dstEid) != bytes32(0), "LZUpdateRateLimits/no-peer-set-for-dstEid");

RateLimitConfig[] memory inboundCfg = new RateLimitConfig[](1);
RateLimitConfig[] memory outboundCfg = new RateLimitConfig[](1);

inboundCfg[0] = RateLimitConfig(dstEid, inboundWindow, inboundLimit);
outboundCfg[0] = RateLimitConfig(dstEid, outboundWindow, outboundLimit);
oft.setRateLimits(inboundCfg, outboundCfg);
}
}
104 changes: 104 additions & 0 deletions deploy/LZHelpers.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.22;

interface ChainlogLike {
function getAddress(bytes32) external view returns (address);
}

struct TxParams {
uint32 dstEid;
bytes32 dstTarget;
bytes dstCallData;
bytes extraOptions;
}

struct MessagingFee {
uint256 nativeFee;
uint256 lzTokenFee;
}

interface GovOAppSenderLike {
function quoteTx(TxParams calldata params, bool payInLzToken) external view returns (MessagingFee memory);
}

interface L2GovernanceRelayLike {
function relay(address target, bytes calldata data) external;
}

interface L2UpdateOFTRateLimitsSpellLike {
function execute(
uint32 dstEid,
address oftAdapter,
uint48 inboundWindow,
uint256 inboundLimit,
uint48 outboundWindow,
uint256 outboundLimit
) external;
}

interface L1GovernanceRelayLike {
function relayEVM(
uint32 dstEid,
address l2GovernanceRelay,
address target,
bytes calldata targetData,
bytes calldata extraOptions,
MessagingFee calldata fee,
address refundAddress
) external payable;
}

library LZHelpers {

ChainlogLike internal constant chainlog = ChainlogLike(0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F);

function relayUpdateOFTRateLimits(
uint32 dstEid,
address l2GovernanceRelay,
address l2Spell,
uint128 l2GasAmount,
address oftAdapter,
uint32 rateLimitDstEid,
uint48 inboundWindow,
uint256 inboundLimit,
uint48 outboundWindow,
uint256 outboundLimit
) internal {
require(l2GasAmount > 0, "RelayUpdateOFTRateLimits/L2-gas-amount-0");

bytes memory spellData = abi.encodeCall(
L2UpdateOFTRateLimitsSpellLike.execute,
(rateLimitDstEid, oftAdapter, inboundWindow, inboundLimit, outboundWindow, outboundLimit)
);

// Equivalent to OptionsBuilder.newOptions().addExecutorLzReceiveOption(optionsGas, 0)
// source: https://github.com/LayerZero-Labs/LayerZero-v2/blob/9c741e7f9790639537b1710a203bcdfd73b0b9ac/packages/layerzero-v2/evm/oapp/contracts/oapp/libs/OptionsBuilder.sol#L139-L145
bytes memory extraOptions = abi.encodePacked(
hex"0003", // OPTIONS_TYPE_3
uint8(1), // WORKER_ID (executor)
uint16(16 + 1), // uint128 gas amount (16 bytes) + uint8 option type (1 byte)
uint8(1), // OPTION_TYPE_LZRECEIVE
l2GasAmount
);

TxParams memory txParams = TxParams({
dstEid: dstEid,
dstTarget: bytes32(uint256(uint160(address(l2GovernanceRelay)))),
dstCallData: abi.encodeCall(L2GovernanceRelayLike.relay, (address(l2Spell), spellData)),
extraOptions: extraOptions
});

MessagingFee memory fee = GovOAppSenderLike(chainlog.getAddress("LZ_GOV_SENDER")).quoteTx(txParams, false);

require(address(this).balance >= fee.nativeFee, "RelayUpdateOFTRateLimits/Insufficient-ETH");
L1GovernanceRelayLike(chainlog.getAddress("LZ_GOV_RELAY")).relayEVM{value: fee.nativeFee}(
dstEid,
l2GovernanceRelay,
address(l2Spell),
spellData,
extraOptions,
fee,
address(this)
);
}
}
8 changes: 6 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
src = "deploy"
out = "out"
libs = ["lib"]
via_ir = true
# via_ir = true
optimizer = false
optimizer_runs = 200

remappings = [
'xchain-helpers/=lib/xchain-helpers/src/',
'@layerzerolabs/lz-evm-protocol-v2/=lib/xchain-helpers/lib/LayerZero-v2/packages/layerzero-v2/evm/protocol/',
'@layerzerolabs/lz-evm-messagelib-v2/=lib/xchain-helpers/lib/LayerZero-v2/packages/layerzero-v2/evm/messagelib/',
'layerzerolabs/oapp-evm/=lib/xchain-helpers/lib/devtools/packages/oapp-evm/',
'@layerzerolabs/oapp-evm/=lib/xchain-helpers/lib/devtools/packages/oapp-evm/',
'@layerzerolabs/script-devtools-evm-foundry/=lib/xchain-helpers/lib/devtools/packages/script-devtools-evm-foundry/',
'@layerzerolabs/oft-evm/=lib/xchain-helpers/lib/devtools/packages/oft-evm/',
'@openzeppelin/contracts/=lib/xchain-helpers/lib/openzeppelin-contracts/contracts/',
'openzeppelin-contracts/contracts/=lib/xchain-helpers/lib/openzeppelin-contracts/contracts/',
Expand Down
41 changes: 41 additions & 0 deletions script/SkyLinkGasProfiler.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import { L2UpdateOFTRateLimitsSpell } from "deploy/L2UpdateOFTRateLimitsSpell.sol";

import { GasProfilerScript, TestParams } from "@layerzerolabs/script-devtools-evm-foundry/scripts/GasProfiling/GasProfiler.s.sol";

contract SkyLinkGasProfiler is GasProfilerScript {

uint32 constant ETH_EID = 30101;
bytes32 constant ETH_GOV_SENDER = bytes32(uint256(uint160(0x27FC1DD771817b53bE48Dc28789533BEa53C9CCA)));

GasProfilerScript public gasProfiler = new GasProfilerScript();

/// @notice Profile rate limits update spell lzReceive gas
/// @dev Run with: REMOTE_RPC_URL=<rpc_url> forge script script/SkyLinkGasProfiler.s.sol:SkyLinkGasProfiler --sig "run_rate_limits_update_spell(uint32,address,address,address)" <dstEid> <receiver> <remoteEndpoint> <usdsOftAddress>
function run_rate_limits_update_spell(uint32 dstEid, address receiver, address remoteEndpoint, address usdsOftAddress) external {
bytes[] memory payloads = new bytes[](1);

// payload for calling rate limit spell
payloads[0] = abi.encodeCall(
L2UpdateOFTRateLimitsSpell.execute,
(ETH_EID, usdsOftAddress, 24 hours, 50_000_000, 24 hours, 50_000_000)
);

TestParams memory params = TestParams({
srcEid: ETH_EID,
sender: ETH_GOV_SENDER,
dstEid: dstEid,
receiver: receiver,
payloads: payloads,
msgValue: 0,
numOfRuns: 5
});

gasProfiler.run_lzReceive(_getRemoteRpcUrl(), remoteEndpoint, params);
}

function _getRemoteRpcUrl() private view returns (string memory) {
return vm.envString("REMOTE_RPC_URL");
}
}
138 changes: 138 additions & 0 deletions test/L2UpdateOFTRateLimitsSpell.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.22;

import "forge-std/Test.sol";

import { L2UpdateOFTRateLimitsSpell } from "deploy/L2UpdateOFTRateLimitsSpell.sol";
import { LZHelpers, ChainlogLike } from "deploy/LZHelpers.sol";

import { Bridge } from "xchain-helpers/testing/Bridge.sol";
import { Domain, DomainHelpers } from "xchain-helpers/testing/Domain.sol";
import { LZBridgeTesting } from "xchain-helpers/testing/bridges/LZBridgeTesting.sol";

interface OAppReadLike {
function peers(uint32 eid) external view returns (bytes32);
function owner() external view returns (address);
}

interface OFTReadLike is OAppReadLike {
function outboundRateLimits(uint32 eid) external view returns (uint128 lastUpdated, uint48 window, uint256 amountInFlight, uint256 limit);
function inboundRateLimits(uint32 eid) external view returns (uint128 lastUpdated, uint48 window, uint256 amountInFlight, uint256 limit);
}

/*** Test contract ***/
contract LZUpdateRateLimitsTest is Test {

using DomainHelpers for *;
using LZBridgeTesting for *;

ChainlogLike constant chainlog = ChainlogLike(0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F);

uint48 inboundWindow = 24 hours;
uint256 inboundLimit = 50_000_000 ether;
uint48 outboundWindow = 24 hours;
uint256 outboundLimit = 50_000_000 ether;

address MCD_PAUSE_PROXY;
address LZ_GOV_SENDER;
address LZ_GOV_RELAY;
address USDS_OFT;

address govOAppReceiver;
address l2GovRelay;
address usdsMintBurn;
L2UpdateOFTRateLimitsSpell l2Spell;

uint32 constant ETHEREUM_EID = 30101;
uint32 constant AVALANCHE_EID = 30106;

Domain mainnet;
Domain avalanche;
Bridge bridge;

function setUp() public {
// Set up the bridge (pinned to the block after ethereum <> avalanche wire is done from spell)
mainnet = getChain("mainnet").createSelectFork(24871364);
avalanche = getChain("avalanche").createFork(82186129);
bridge = mainnet.createLZBridge(avalanche);

mainnet.selectFork();

MCD_PAUSE_PROXY = chainlog.getAddress("MCD_PAUSE_PROXY");
LZ_GOV_SENDER = chainlog.getAddress("LZ_GOV_SENDER");
LZ_GOV_RELAY = chainlog.getAddress("LZ_GOV_RELAY");
USDS_OFT = chainlog.getAddress("USDS_OFT");

// Set up remote addresses
govOAppReceiver = bytes32ToAddress(OAppReadLike(LZ_GOV_SENDER).peers(AVALANCHE_EID));
usdsMintBurn = bytes32ToAddress(OFTReadLike(USDS_OFT).peers(AVALANCHE_EID));

avalanche.selectFork();
// Deploy the L2 spell
l2Spell = new L2UpdateOFTRateLimitsSpell();

// Set up remote addresses from avalanche
l2GovRelay = OAppReadLike(govOAppReceiver).owner();

mainnet.selectFork();
}

function test_setRateLimits() public {
// Donate some ETH to pay LZ fee
vm.deal(address(MCD_PAUSE_PROXY), 0.00003 ether);

// Check state before relay
avalanche.selectFork();
( , uint48 inWindowBefore, , uint256 inLimitBefore) = OFTReadLike(usdsMintBurn).inboundRateLimits(ETHEREUM_EID);
( , uint48 outWindowBefore, , uint256 outLimitBefore) = OFTReadLike(usdsMintBurn).outboundRateLimits(ETHEREUM_EID);
assertNotEq(inLimitBefore, inboundLimit);
assertNotEq(outLimitBefore, outboundLimit);
// Check rateLimits window values are not already set
assertEq(inWindowBefore, inboundWindow);
assertEq(outWindowBefore, outboundWindow);

// Execute core spell and relay messages
mainnet.selectFork();
vm.startPrank(MCD_PAUSE_PROXY);
LZHelpers.relayUpdateOFTRateLimits(
AVALANCHE_EID,
l2GovRelay,
address(l2Spell),
uint128(100_000),
usdsMintBurn,
ETHEREUM_EID,
inboundWindow,
inboundLimit,
outboundWindow,
outboundLimit
);
vm.stopPrank();
bridge.relayMessagesToDestination(true, LZ_GOV_SENDER, address(govOAppReceiver));

// Check updated rate limits on the OFT adapter
( , uint48 inWindow, , uint256 inLimit) = OFTReadLike(usdsMintBurn).inboundRateLimits(ETHEREUM_EID);
( , uint48 outWindow, , uint256 outLimit) = OFTReadLike(usdsMintBurn).outboundRateLimits(ETHEREUM_EID);
assertEq(inWindow, inboundWindow);
assertEq(inLimit, inboundLimit);
assertEq(outWindow, outboundWindow);
assertEq(outLimit, outboundLimit);
}

function test_setRateLimits_revertsIfNoPeerSet() public {
uint32 baseEid = 30184;
// Switch to Avalanche and test the spell directly
avalanche.selectFork();

// Pre-condition: peer is not set
assertEq(OFTReadLike(usdsMintBurn).peers(baseEid), bytes32(0), "peer must not be set");

// Expect the spell to revert when peer is not set
vm.expectRevert("LZUpdateRateLimits/no-peer-set-for-dstEid");
vm.prank(l2GovRelay);
l2Spell.execute(baseEid, usdsMintBurn, inboundWindow, inboundLimit, outboundWindow, outboundLimit);
}

function bytes32ToAddress(bytes32 b) internal pure returns (address) {
return address(uint160(uint256(b)));
}
}