Skip to content

feat: fix rate limit for transferTokenLayerZero (DEV-1131)#227

Open
deluca-mike wants to merge 5 commits intodevfrom
fix/rate-limit-for-transferTokenLayerZero
Open

feat: fix rate limit for transferTokenLayerZero (DEV-1131)#227
deluca-mike wants to merge 5 commits intodevfrom
fix/rate-limit-for-transferTokenLayerZero

Conversation

@deluca-mike
Copy link
Collaborator

@deluca-mike deluca-mike commented Jan 29, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Refined rate limiting mechanism to calculate limits based on actual token balance changes
    • Enhanced quote amount derivation for improved accuracy in cross-chain token transfers
    • Optimized internal helper functions for efficiency
  • Tests

    • Updated test cases and validation to verify refined rate limiting and quote calculation logic

✏️ Tip: You can customize this high-level summary in your review settings.

@octane-security-app
Copy link

Summary by Octane

New Contracts

No new contracts were added.

Updated Contracts

  • LayerZeroLib.sol: The smart contract now checks token balance changes for rate limiting and improves send parameter setup by refactoring functions.

🔗 Commit Hash: 11c1cd0

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

This PR modifies the LayerZero token transfer library to calculate rate limits based on actual post-transfer balance deltas instead of pre-call estimates, introduces a helper function to extract quoted amounts, and updates corresponding tests to reflect the new rate limiting behavior.

Changes

Cohort / File(s) Summary
LayerZero Library Rate Limiting
src/libraries/LayerZeroLib.sol
Shifted rate limiting from pre-call to post-transfer calculation using balance delta; introduced _getQuotedAmountReceivedLD helper to extract quoted amounts from quoteOFT; added balanceBefore capture and local token variable for consistency.
LayerZero Test Suite
test/mainnet-fork/LayerZero.t.sol
Updated test setup, reorganized test cases to align with new post-transfer rate limiting; added recipient-not-set path; adjusted zero/max amount and rate-limit boundary scenarios; updated event signature formatting for OFTSent.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Proxy
    participant RateLimiter
    participant Token

    rect rgba(100, 150, 200, 0.5)
        Note over Caller,Token: Old Flow: Pre-Transfer Rate Check
        Caller->>RateLimiter: Check rate limit (pre-call)
        RateLimiter-->>Caller: Verify limit not exceeded
        Caller->>Proxy: Execute transfer
        Proxy->>Token: Transfer tokens
    end

    rect rgba(150, 200, 100, 0.5)
        Note over Caller,Token: New Flow: Post-Transfer Balance Delta
        Caller->>Proxy: Capture balance before
        Caller->>Proxy: Execute transfer
        Proxy->>Token: Transfer tokens
        Token-->>Proxy: Confirm transfer
        Caller->>Proxy: Calculate delta (before - after)
        Caller->>RateLimiter: Apply rate limit with actual delta
        RateLimiter-->>Caller: Verify limit not exceeded
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • supercontracts
  • lucas-manuel

Poem

🐰 Hop, skip, and balance! The rate limits now dance,
Post-transfer deltas guide each token's advance.
With helpers extracted and balance stored true,
LayerZero flows smoother—fresh logic, brand new!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title follows the required format with a type prefix ('feat:') and concisely describes the main change: fixing rate limit calculation in transferTokenLayerZero, with JIRA key included.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/rate-limit-for-transferTokenLayerZero

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/libraries/LayerZeroLib.sol`:
- Around line 57-75: The subtraction of balances can underflow if balanceAfter >
balanceBefore; compute balanceAfter =
IERC20Like(token).balanceOf(address(proxy)) after the proxy.doCallWithValue
call, then compute uint256 delta = balanceBefore > balanceAfter ? balanceBefore
- balanceAfter : 0 and pass delta into _rateLimited; update the call site that
currently computes balanceBefore - IERC20Like(token).balanceOf(address(proxy))
(involving IERC20Like, proxy.doCallWithValue, _rateLimited,
LIMIT_LAYERZERO_TRANSFER, oftAddress, destinationEndpointId, token) to use the
clamped delta instead to avoid revert on rebasing/reflective tokens.
- Around line 33-57: Add a guard after retrieving the token from
ILayerZero(oftAddress).token() to ensure token is not the zero address; if token
== address(0) revert with a clear error like "LayerZeroLib/invalid-token" so
subsequent calls (ApproveLib.approve(...), IERC20Like(token).balanceOf(...), and
any ERC20 interactions) cannot revert unexpectedly; update the code around the
token variable in LayerZeroLib so the require check runs immediately after
calling ILayerZero.token() and before any approve or balanceOf usage.

In `@test/mainnet-fork/LayerZero.t.sol`:
- Around line 206-212: The event declaration for OFTSent is syntactically
invalid due to a missing comma after the dstEid parameter; update the OFTSent
event signature (event OFTSent(... dstEid ... , address indexed fromAddress,
...)) by inserting the comma after dstEid so the parameter list is properly
comma-separated, then recompile to verify the error is resolved.

Comment on lines +33 to +57
address token = ILayerZero(oftAddress).token();

// NOTE: Full integration testing of this logic is not possible without OFTs with
// approvalRequired == false. Add integration testing for this case before
// using in production.
if (ILayerZero(oftAddress).approvalRequired()) {
ApproveLib.approve(
ILayerZero(oftAddress).token(),
address(proxy),
oftAddress,
amount
);
ApproveLib.approve(token, address(proxy), oftAddress, amount);
}

bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0);

SendParam memory sendParams = SendParam({
dstEid : destinationEndpointId,
to : layerZeroRecipient,
amountLD : amount,
minAmountLD : 0,
extraOptions : options,
extraOptions : OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0),
composeMsg : "",
oftCmd : ""
});

// Query the min amount received on the destination chain and set it.
( , , OFTReceipt memory receipt ) = ILayerZero(oftAddress).quoteOFT(sendParams);
sendParams.minAmountLD = receipt.amountReceivedLD;
sendParams.minAmountLD = _getQuotedAmountReceivedLD(oftAddress, sendParams);

MessagingFee memory fee = ILayerZero(oftAddress).quoteSend(sendParams, false);

uint256 balanceBefore = IERC20Like(token).balanceOf(address(proxy));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

LayerZero OFT token() function return value address zero

💡 Result:

In LayerZero’s OFT (V2 / Endpoint V2) contracts, token() is meant to return the underlying ERC20 address:

  • OFT.sol (a “native” OFT that is the ERC20): token() returns address(this) (the OFT contract address). [1]
  • OFTAdapter.sol (wraps an existing ERC20): token() returns the adapted ERC20 (innerToken). [2]
  • LayerZero’s own contract playground also labels token() as “Get Underlying Token Address”. [3]

So if you’re seeing address(0), it’s not expected for a standard OFT or OFTAdapter. The usual causes are:

  1. Wrong contract / wrong inheritance: you’re calling token() on something that isn’t OFT/OFTAdapter (or a custom contract that didn’t override token() as those do).
  2. ABI mismatch / wrong address: your interface/ABI doesn’t match the deployed bytecode you’re querying (common when mixing OFT packages/versions), or you’re querying the wrong deployed address.

To debug quickly: verify the contract source on the explorer and confirm it is @layerzerolabs/oft-evm OFT or OFTAdapter, then call approvalRequired()—it’s typically false for OFT and true for the default OFTAdapter, which helps confirm what you’re interacting with. [3]

Sources: [1] [2] [3]


🌐 Web query:

LayerZero OFTAdapter OFT token() API contract documentation

💡 Result:

LayerZero OFTAdapter.token() API (EVM, V2 OFT)

Signature

  • function token() public view returns (address)

What it returns

  • In OFTAdapter, token() returns the underlying (adapted) ERC‑20 token address (address(innerToken)). This is explicitly because, for an adapter, address(this) (the adapter) is not the ERC‑20 token contract. [1]

Contrast with OFT (non-adapter)

  • In OFT, token() returns address(this) (because the OFT contract is the ERC‑20). [2]

Where this fits conceptually

  • LayerZero’s docs describe OFTAdapter as the approach for existing ERC‑20s (typically lock/unlock semantics) without modifying the original token contract. [3][4]

Sources

  1. Verified OFTAdapter implementation showing token() returns address(innerToken) (Etherscan). [1]
  2. OFT implementation snippet showing token() returns address(this) (layerzero.org). [2]
  3. LayerZero V2 OFT Quickstart (OFT vs OFTAdapter overview). [3]
  4. OFT Technical Reference (adapter pattern explanation). [4]

🏁 Script executed:

# Search for LayerZero interface definitions in the repository
fd -e sol | head -20

Repository: sparkdotfi/spark-alm-controller

Length of output: 647


🏁 Script executed:

# Search for ILayerZero interface definition
rg "interface ILayerZero" -A 20

Repository: sparkdotfi/spark-alm-controller

Length of output: 1385


🏁 Script executed:

# Search for LayerZero OFT token() related code
rg "\.token\(\)" --type sol -B 2 -A 2

Repository: sparkdotfi/spark-alm-controller

Length of output: 102


🏁 Script executed:

# Look at the LayerZeroLib.sol file to understand context
cat -n src/libraries/LayerZeroLib.sol | head -100

Repository: sparkdotfi/spark-alm-controller

Length of output: 4510


🏁 Script executed:

# Search for where LayerZeroLib is used and with what OFT implementations
rg "LayerZeroLib" -A 5 -B 5 --type sol

Repository: sparkdotfi/spark-alm-controller

Length of output: 102


🏁 Script executed:

# Check for token address validation patterns
rg "token.*address\(0\)" --type sol

Repository: sparkdotfi/spark-alm-controller

Length of output: 102


🏁 Script executed:

# Search for OFT instantiation or deployment in tests/scripts
rg "new OFT\|OFTAdapter\|token()" --type sol -B 2 -A 2 | head -100

Repository: sparkdotfi/spark-alm-controller

Length of output: 102


🏁 Script executed:

# Check if there are any checks for address(0) in the codebase
rg "== address\(0\)\|!= address\(0\)" --type sol | head -20

Repository: sparkdotfi/spark-alm-controller

Length of output: 102


🏁 Script executed:

# Search for ApproveLib to understand what it does
cat -n src/libraries/ApproveLib.sol | head -50

Repository: sparkdotfi/spark-alm-controller

Length of output: 1770


🏁 Script executed:

# Search for how oftAddress is validated
rg "oftAddress" --type-list

Repository: sparkdotfi/spark-alm-controller

Length of output: 464


🏁 Script executed:

# Use rg without type filter
rg "oftAddress" -B 3 -A 3 | head -150

Repository: sparkdotfi/spark-alm-controller

Length of output: 7080


🏁 Script executed:

# Search for any address(0) checks in the codebase
rg "address\(0\)" | head -30

Repository: sparkdotfi/spark-alm-controller

Length of output: 2952


🏁 Script executed:

# Check the imports and understand IERC20Like
rg "IERC20Like" -B 2 -A 5

Repository: sparkdotfi/spark-alm-controller

Length of output: 9084


Add a guard against token == address(0) to prevent reverts in ERC20 calls.

While standard LayerZero OFT and OFTAdapter implementations never return address(0) from token(), the interface provides no guarantee. If an OFT variant or misconfigured contract returns zero, the subsequent approve() and balanceOf() calls will revert. Add a require statement at line 33 to validate the token address:

address token = ILayerZero(oftAddress).token();
require(token != address(0), "LayerZeroLib/invalid-token");

Do not fall back to oftAddress as suggested—for OFT contracts, token() returns address(this) (the OFT itself), and for OFTAdapter, the fallback would be incorrect.

🤖 Prompt for AI Agents
In `@src/libraries/LayerZeroLib.sol` around lines 33 - 57, Add a guard after
retrieving the token from ILayerZero(oftAddress).token() to ensure token is not
the zero address; if token == address(0) revert with a clear error like
"LayerZeroLib/invalid-token" so subsequent calls (ApproveLib.approve(...),
IERC20Like(token).balanceOf(...), and any ERC20 interactions) cannot revert
unexpectedly; update the code around the token variable in LayerZeroLib so the
require check runs immediately after calling ILayerZero.token() and before any
approve or balanceOf usage.

@octane-security-app
Copy link

Overview

Vulnerabilities found: 1                                                                                
Warnings found: 1                                                                                

🔗 Commit Hash: 11c1cd0
🛡️ Octane Dashboard: All vulnerabilities

@lucas-manuel lucas-manuel changed the title feat: fix rate limit for transferTokenLayerZero feat: fix rate limit for transferTokenLayerZero (DEV-1131) Jan 29, 2026
@notion-workspace
Copy link

),
amount
);
require(layerZeroRecipient != bytes32(0), "MC/recipient-not-set");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LayerZeroLib

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lucas-manuel None of our libs use the lib name in the error message. Is that what we what to adopt going forward?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first lib thats being used by both controllers, so yeah this one can first then the rest

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

10_000_000e6 + 1,
destinationEndpointId
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix error message

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

vm.expectRevert("LayerZeroLib/recipient-not-set");
foreignController.transferTokenLayerZero{value: fee.nativeFee}(
USDT_OFT,
10_000_000e6,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many extra lines after last function bracket and last contract brancket

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where.

@github-actions
Copy link

Coverage after merging fix/rate-limit-for-transferTokenLayerZero into dev will be

99.30%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
deploy
   ControllerDeploy.sol100%100%100%100%
   ForeignControllerInit.sol100%100%100%100%
   MainnetControllerInit.sol97.37%93.33%100%100%152, 90
src
   ALMProxy.sol100%100%100%100%
   ALMProxyFreezable.sol100%100%100%100%
   ForeignController.sol96.10%89.66%95.65%98.04%130–131, 131, 131, 556
   MainnetController.sol99.18%100%98.36%99.24%596–597
   OTCBuffer.sol92.31%100%83.33%93.75%55
   RateLimitHelpers.sol100%100%100%100%
   RateLimits.sol100%100%100%100%
   WEETHModule.sol93.18%90%85.71%96.30%65, 72
src/libraries
   AaveLib.sol100%100%100%100%
   ApproveLib.sol100%100%100%100%
   CCTPLib.sol100%100%100%100%
   CurveLib.sol100%100%100%100%
   ERC4626Lib.sol97.06%90%100%100%117
   LayerZeroLib.sol100%100%100%100%
   PSMLib.sol100%100%100%100%
   UniswapV4Lib.sol99.32%95.65%100%100%282
   WEETHLib.sol100%100%100%100%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants