Ondo Finance - K42's results

Institutional-Grade Finance. On-Chain. For Everyone.

General Information

Platform: Code4rena

Start Date: 01/09/2023

Pot Size: $36,500 USDC

Total HM: 4

Participants: 70

Period: 6 days

Judge: kirk-baird

Id: 281

League: ETH

Ondo Finance

Findings Distribution

Researcher Performance

Rank: 30/70

Findings: 2

Award: $130.92

Gas:
grade-a
Analysis:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

112.0737 USDC - $112.07

Labels

bug
G (Gas Optimization)
grade-a
sponsor acknowledged
sufficient quality report
G-18

External Links

Gas Optimization Report for Ondo by K42

Possible Optimization in SourceBridge.sol

Possible Optimization 1 =

Here is the optimized code snippet:

mapping(string => string) public immutable destChainToContractAddr;
  • Estimated gas saved = Around ~100 gas for SLOAD opcode savings per access.

Possible Optimization 2 =

  • In burnAndCallAxelar() you could batch multiple state variable updates into a single external function call, to reduce the gas cost of multiple SSTORE operations.

Here is the optimized code:

function burnAndCallAxelar(
    uint256 amount,
    string calldata destinationChain,
    uint256 newNonce
) external payable whenNotPaused {
    // ... existing logic
    nonce = newNonce;
}
  • Estimated gas saved = Around ~5000 gas for reduced SSTORE operations.

Possible Optimization 3 =

  • You could batch msg.sender and msg.value into a single struct to reduce stack depth.

After Optimization:

struct MsgData {
    address sender;
    uint256 value;
}

function burnAndCallAxelar(
    uint256 amount,
    string calldata destinationChain,
    MsgData calldata msgData
) external payable whenNotPaused {
    // Then use msgData.sender and msgData.value instead of msg.sender and msg.value here
}
  • Estimated gas saved = ~1000-2000 gas (stack operations usually cost around 3 gas for PUSH and 3 gas for POP).

Possible Optimization 4 =

  • Use bytes32 for destinationChain instead of string to save gas on storage and comparison.

After Optimization:

mapping(bytes32 => string) public destChainToContractAddr;

function burnAndCallAxelar(
    uint256 amount,
    bytes32 destinationChain
) external payable whenNotPaused {
    // ...
}
  • Estimated gas saved = ~500-1000 gas (SSTORE costs 20,000 gas for a new value and 5,000 gas for updating, while SLOAD costs 800 gas).

Possible Optimization 5 =

After Optimization:

function multiexcall(
    ExCallData[] calldata exCallData
) external payable override onlyOwner returns (bytes[] memory results) {
    results = new bytes[](exCallData.length);
    for (uint256 i = 0; i < exCallData.length; ++i) {
        (bool success, bytes memory ret) = address(exCallData[i].target).call{
            value: exCallData[i].value
        }(exCallData[i].data);
        require(success, "Call Failed");
        results[i] = ret;
    }
}
  • Estimated gas saved = Around ~1,000 gas due to reduced loop iterations and direct opcode usage.

Possible Optimizations in DestinationBridge.sol

Possible Optimization 1 =

  • Use staticcall for read-only external calls like ALLOWLIST.isAllowed(), as staticcall is cheaper than a regular call for read-only operations.

After Optimization:

(bool success, bytes memory data) = address(ALLOWLIST).staticcall(abi.encodeWithSignature("isAllowed(address)", txn.sender));
  • Estimated gas saved = ~2000-3000 gas.

Possible Optimization 2 =

After Optimization:

struct Threshold {
    uint128 amount;
    uint128 numberOfApprovalsNeeded;
}

struct TxnThreshold {
    uint128 numberOfApprovalsNeeded;
    address[] approvers;
}
  • Estimated gas saved = ~5000 gas for reduced SSTORE operations.

Possible Optimization 3 =

  • If the length of srcChain and srcAddr strings are within 32 bytes, using bytes32 can save gas on storage and comparisons.

After Optimization:

function _execute(
  bytes32 srcChain,
  bytes32 srcAddr,
  bytes calldata payload
) internal override whenNotPaused {
  // ...
}
  • Estimated gas saved = ~1000-2000 gas.

Possible Optimization 4 =

After Optimization:

struct TxData {
    Transaction txn;
    TxnThreshold threshold;
}

mapping(bytes32 => TxData) public txnData;
  • Estimated gas saved = ~5000 gas for reduced SSTORE operations.

Possible Optimization 5 =

  • In the _execute() function, batch the state updates at the end of the function to take advantage of the "net gas metering" EIP-1283.

After Optimization:

function _execute(
    // ... existing parameters
) internal override whenNotPaused {
    // ... existing logic
    isSpentNonce[chainToApprovedSender[srcChain]][nonce] = true;
    txnHashToTransaction[txnHash] = Transaction(srcSender, amt);
    // ... existing logic
}
  • Estimated gas saved = ~5000 gas for reduced SSTORE operations.

Possible Optimizations in rUSDY.sol

Possible Optimization 1 =

  • Mark oracle and usdy as immutable to save gas on SLOAD operations, if these addresses are not expected to change after contract deployment.

After Optimization:

IRWADynamicOracle public immutable oracle;
IUSDY public immutable usdy;
  • Estimated gas saved = ~800 gas per SLOAD access.

Possible Optimization 2 =

After Optimization:

function _mintShares(address _recipient, uint256 _sharesAmount) internal whenNotPaused returns (uint256) {
    // ... existing logic
    totalShares += _sharesAmount;
    shares[_recipient] += _sharesAmount;
    // ... existing logic
}
  • Estimated gas saved = ~5,000 gas for reduced SSTORE operations.

Possible Optimization 3 =

  • Use staticcall for read-only external calls like oracle.getPrice() also, as staticcall is cheaper than a regular call.

After Optimization:

(bool success, bytes memory data) = address(oracle).staticcall(abi.encodeWithSignature("getPrice()"));
  • Estimated gas saved = ~2000-3000 gas.

Possible Optimizations in RWADynamicOracle.sol

Possible Optimization 1 =

  • Reduce the number of times block.timestamp is accessed in the getPrice() function.

After Optimization:

function getPrice() public view whenNotPaused returns (uint256 price) {
  uint256 currentTimestamp = block.timestamp;  // Cache block.timestamp
  uint256 length = ranges.length;
  for (uint256 i = 0; i < length; ++i) {
    Range storage range = ranges[(length - 1) - i];
    if (range.start <= currentTimestamp) {
      if (range.end <= currentTimestamp) {
        return derivePrice(range, range.end - 1);
      } else {
        return derivePrice(range, currentTimestamp);
      }
    }
  }
}
  • Estimated gas saved = ~200-300 gas per getPrice() call.

Possible Optimization 2 =

After Optimization:

struct Range {
  uint64 start;
  uint64 end;
  uint64 dailyInterestRate;
  uint64 prevRangeClosePrice;
}
  • Estimated gas saved = ~15,000 gas per setRange() call.

#0 - c4-pre-sort

2023-09-08T14:27:35Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-09-24T06:08:55Z

kirk-baird marked the issue as grade-a

#2 - c4-sponsor

2023-09-27T21:27:43Z

ali2251 (sponsor) acknowledged

Findings Information

Awards

18.8458 USDC - $18.85

Labels

analysis-advanced
grade-b
sufficient quality report
A-12

External Links

Advanced Analysis Report for Ondo by K42

Overview

  • Ondo Ondo is a DeFi platform that specializes in yield optimization and asset management, with a focus on cross-chain interoperability. For this audit, I focused on gas optimizations, therefore security recommendations are brief in this analysis.

Understanding the Ecosystem:

Key Contracts:

Inter-Contract Communication:

Codebase Quality Analysis:

Security:

  • Role-based access control is implemented well.
  • Use of OpenZeppelin libraries for standard functionalities like ERC20.

Architecture Recommendations:

  • Implement a proxy pattern for contract upgradability to allow for future improvements without requiring a redeployment of the entire contract.
  • Consider using a Layer 2 solution like Optimism or zk-Rollups for scalability, especially for the bridge contracts.

Centralization Risks:

  • RWADynamicOracle.sol serves as a single point of failure. If compromised, it could manipulate asset prices.
  • Admin roles have the ability to pause contracts and modify key parameters, such as interest rates in rUSDY.sol, without community input.

Mechanism Review:

Asset Transfer:

  • Secure but dependent on external oracles.
  • Needs more decentralization in oracle selection.

Token Management:

  • Factory pattern used for token creation.
  • Consider adding a token burning mechanism.

Role-Based Access Control:

  • Implemented but could be optimized for gas.
  • Consider using a more dynamic role management system.

Systemic Risks:

Specific Risks and Mitigations in Key Contracts:

SourceBridge.sol:

  • Risks: Front-running attacks during asset transfer.
  • Mitigations: Implement a commit-reveal scheme.

DestinationBridge.sol:

  • Risks: Incorrect asset mapping.
  • Mitigations: Double-checking through off-chain services.

RWADynamicOracle.sol:

  • Risks: Oracle manipulation.
  • Mitigations: Multiple data sources and median calculation.

Areas of Concern

  • Oracle dependency.
  • Lack of upgradability.
  • Centralized role management.

Recommendations

  • Implement Chainlink oracles for more secure and decentralized price feeds.
  • Implement a DAO for community governance.

Contract Details

Here are function interaction graphs for each contract I made to better visualize interactions:

Conclusion

  • While Ondo's architecture is robust in its core functionalities, it has several areas that require attention, particularly in terms of decentralization, security, and upgradability. These improvements are crucial for mitigating systemic risks and ensuring long-term sustainability.

Time spent:

20 hours

#0 - c4-pre-sort

2023-09-08T14:43:59Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-09-24T07:12:35Z

kirk-baird marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter