LayerZero logo

LayerZero OFT Rate Limiter Security Review Report

May 2024

Overview

This audit covered the newly developed OFT Rate Limiter smart contracts for LayerZero. These contracts serve as an example of how a rate limit between chains could be implemented.

Our security assessment was a full review of the 2 smart contract, spanning a total of 3 days.

During our audit, we have identified 1 medium severity vulnerability, various minor vulnerabilities and code optimisations.

Finally, all of our reported issues were fixed or acknowledged by the development team and consequently validated by us.

We can confidently say that the overall security and code quality have increased after completion of our audit.

Scope

The analyzed resources are located on:

https://github.com/LayerZero-Labs/ondo-oft-rate-limiter/tree/fcf731a47bda74572ed0e54ea2c23cfeddaf458c

The issues described in this report were fixed in the following commit:

https://github.com/LayerZero-Labs/ondo-oft-rate-limiter/tree/d4a52a71b14c230c350e64cfdb5cc19c9d07a840

Summary

Total number of findings
4

Weaknesses

This section contains the list of discovered weaknesses.

LZ01-1 | FUNDS CAN BE STUCK BECAUSE THE LIMITS ARE NOT SYNCHRONIZED AND CAN BE CHANGED AFTER A USER TX

Severity:

Medium

Status:

Acknowledged

Path:

contracts/OFTEfficientRateLimit.sol:L127-L137

Description:

The limits can be different between chains where the OFT contracts are deployed.

If the source chain allows sending more than the destination allows receiving, transactions with an amount between these two limits can be sent but cannot be received on the destination chain because the corresponding transactions to lzReceive() will revert.

// Check if the requested amount exceeds the available capacity
if (_amount > availableCapacity) revert RateLimitExceeded();

The issue will only be triggered by a user error or if the limit is changed after a transaction is sent on the source chain but before its execution on the destination.

There's no mechanism to rescue the funds other than the owners manually setting the limit to the required amount, which can be impossible or harmful in some situations.

function _lzReceive(
    Origin calldata _origin,
    bytes32 _guid,
    bytes calldata _message,
    address _executor,
    bytes calldata _extraData
) internal virtual override {
    // Check and update the rate limit based on the source endpoint ID (srcEid) and the amount in local decimals from the message.
    _checkAndUpdateRateLimit(_origin.srcEid, _toLD(OFTMsgCodec.amountSD(_message)), RateLimitDirection.Inbound);
    super._lzReceive(_origin, _guid, _message, _executor, _extraData);
}

Remediation:

One solution is to implement a queue with a gradual release of funds in case the total amount exceeds the limit.

LZ01-2 | REFACTORING DECAY CALCULATION LOGIC IN EFFICIENTRATELIMITER

Severity:

Informational

Status:

Fixed

Path:

contracts/EfficientRateLimiter.sol

Description:

The EfficientRateLimiter.sol contract implements rate limiting functionality to control how often specific actions can be executed. It contains logic to manage rate limits for both inbound and outbound transactions, ensuring that usage does not exceed predefined thresholds within specified time windows. However, the current implementation suffers from code duplication in the decay calculation logic across multiple functions.

The decay calculation logic is duplicated in the following functions:

  • _amountCanBeSent()
  • _amountCanBeReceived()
  • _calculateLimits()
uint256 timeSinceLastUpdate = block.timestamp - lastUpdated;
if (timeSinceLastUpdate >= window) {
    return (0, limit);
} else {
    uint256 decay = (limit * timeSinceLastUpdate) / window;
    currentAmountInFlight = amountInFlight > decay ? amountInFlight - decay : 0;
    availableCapacity = limit > currentAmountInFlight ? limit - currentAmountInFlight : 0;
    return (currentAmountInFlight, availableCapacity);
}

We recommend adding a new helper function, e.g. called _calculateDecay() is created to handle the decay calculations. The function can take the current amount in flight, the last updated timestamp, the limit, and the window duration as parameters, and return the decayed amount in flight and the amount that can be processed (sent or received). This will improve the readability of the contract by reducing code duplication and clearly separating the decay logic.

function _amountCanBeSent(
    uint256 _amountInFlight,
    uint128 _lastUpdated,
    uint64 _limit,
    uint48 _window
) internal view virtual returns (uint256 currentAmountInFlight, uint256 amountCanBeSent) {
    uint256 timeSinceLastDeposit = block.timestamp - _lastUpdated;
    if (timeSinceLastDeposit >= _window) {
        currentAmountInFlight = 0;
        amountCanBeSent = _limit;
    } else {
        // @dev Presumes linear decay.
        uint256 decay = (_limit * timeSinceLastDeposit) / _window;
        currentAmountInFlight = _amountInFlight <= decay ? 0 : _amountInFlight - decay;
        // @dev In the event the _limit is lowered, and the 'in-flight' amount is higher than the _limit, set to 0.
        amountCanBeSent = _limit <= currentAmountInFlight ? 0 : _limit - currentAmountInFlight;
    }
}
function _amountCanBeReceived(
    uint256 _amountInFlight,
    uint128 _lastUpdated,
    uint64 _limit,
    uint48 _window
) internal view virtual returns (uint256 currentAmountInFlight, uint256 amountCanBeReceived) {
    uint256 timeSinceLastDeposit = block.timestamp - _lastUpdated;
    if (timeSinceLastDeposit >= _window) {
        currentAmountInFlight = 0;
        amountCanBeReceived = _limit;
    } else {
        // @dev Presumes linear decay.
        uint256 decay = (_limit * timeSinceLastDeposit) / _window;
        currentAmountInFlight = _amountInFlight <= decay ? 0 : _amountInFlight - decay;
        // @dev In the event the _limit is lowered, and the 'in-flight' amount is higher than the _limit, set to 0.
        amountCanBeReceived = _limit <= currentAmountInFlight ? 0 : _limit - currentAmountInFlight;
    }
}
function _calculateLimits(
    uint256 amountInFlight,
    uint256 lastUpdated,
    uint64 limit,
    uint48 window
) internal view returns (uint256 currentAmountInFlight, uint256 availableCapacity) {
    uint256 timeSinceLastUpdate = block.timestamp - lastUpdated;
    if (timeSinceLastUpdate >= window) {
        return (0, limit);
    } else {
        uint256 decay = (limit * timeSinceLastUpdate) / window;
        currentAmountInFlight = amountInFlight > decay ? amountInFlight - decay : 0;
        availableCapacity = limit > currentAmountInFlight ? limit - currentAmountInFlight : 0;
        return (currentAmountInFlight, availableCapacity);
    }
}

LZ01-3 | MISSING ACCESS CONTROL IN MINT FUNCTION

Severity:

Informational

Status:

Acknowledged

Path:

contracts/mocks/OFTEfficientRateLimitMock.sol

Description:

The OFTEfficientRateLimitMock.sol contract is a mock implementation of the OFTEfficientRateLimit contract, designed for testing purposes. This mock contract includes a mint function that allows for the creation of new tokens.

Although the OFTEfficientRateLimitMock contract is intended solely for testing purposes, having the mint function publicly accessible is a potential security risk, which in this case has no impact. Public access to minting functions can lead to the unauthorized creation of tokens, which could compromise the integrity of the token supply.

function mint(address _to, uint256 _amount) public {
    _mint(_to, _amount);
}

Remediation:

Restrict access to the mint function.

LZ01-4 | ADDING BATCH RATE LIMIT SETTING FUNCTIONALITY TO OFTEFFICIENTRATELIMIT

Severity:

Informational

Status:

Acknowledged

Path:

contracts/OFTEfficientRateLimit.sol

Description:

The OFTEfficientRateLimit.sol contract currently allows setting rate limits through the setRateLimits() function. However, this function processes rate limit configurations one by one, which can be inefficient and cumbersome for large-scale updates. Introducing a batch setting functionality can streamline this process, reducing gas costs and improving usability.

function setRateLimits(RateLimitConfig[] calldata _rateLimitConfigs, RateLimitDirection direction) external onlyOwner {
    _setRateLimits(_rateLimitConfigs, direction);
}

Remediation:

Introduce a batchSetRateLimits function that allows setting multiple rate limit configurations in a single transaction.

Table of contents