Levva Updates and Oracle Feature

QuantstampFiles named in the report

Quantstamp reviewed Levva Protocol (Levva Updates and Oracle Feature) on 25 Apr 2024: 10 findings, 0 critical; 8 marked fixed, 1 acknowledged, 1 open.

No open critical findings
Open the reportOpen Levva Protocol’s security record

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings10

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High001
Medium003
Low114
HighIncorrect Chainlink and Pyth Oracle ImplementationFixed

The `ChainlinkOracle` and `PythOracle` contracts calculate the base prices based on the value returned by the price feeds. However, unlike Uniswap V3 TWAPs, Chainlink or Pyth Network price feeds do not consider the ERC-20 token decimals of the two tokens in the returned price. Therefore, the oracle contracts must scale the returned price based on the differences between the base and quote token’s decimals. Otherwise, an incorrect price will be used if the token’s decimals differ. For example, assume a pool has ETH as the base token and USDC as the quote token, and Chainlink USDC/ETH price feed is configured as the oracle. If the oracle returns an `answer` of `3e14` with `decimals` as 18, since ETH has 18 decimals, and USDC has 6, the final base price should be `10**oracleDecimals / answer * 10**usdcDecimals / 10**ethDecimals = (1e18 / 3e14) * (1e6 / 1e18) = 3.3e-09`, i.e., 1 wei ETH is worth `3.3e-09` wei USDC. It should be noted that the order of `quoteToken` and `baseToken` configured in the oracle may not necessarily be the same as that configured in `MarginlyPool`. Specifically, for the above example, the `baseToken` parameter to the `ChainlinkOracle.setPair()` call should be USDC, and the `quoteToken` parameter should be ETH. Admins should be cautious about this difference when configuring the oracle settings.

MediumInaccurate Interest CalculationFixed

The interest rates for long and short positions are calculated based on the `longX96` and `shortX96` variables when the `accrueInterest()` function is called. Therefore, to maintain an accurate interest calculation, these variables must be updated through the `updateSystemLeverageLong()` and `updateSystemLeverageShort()` functions after the system's overall debt or collateral total value has been changed. The `updateSystemLeverageLong()` and `updateSystemLeverageShort()` functions are called in the `execute()` function after a user action is performed. However, if the caller's position is underwater and liquidated, the system leverage will not be updated, causing inaccuracies when interests are accrued in the next block. For example, when a large long position is liquidated (by the owner calling the `execute()` function), the interest rate owed by longs to shorts should decrease. Without updating the system's long leverage, longs will owe shorts more interest starting from the next block.

MediumIncorrect Accounting in `receivePosition()`Fixed

Whenever a position's debt is adjusted, the collateral part of the position should also be adjusted based on the deleveraging coefficients multiplied by the change in debt. In the `receivePosition()` function, however, the position's collateral values are not correctly adjusted based on the (partially) repaid debt. This can cause overtaken positions to be accounted for more collateral than they should have since the reduction by the deleverage coefficient multiplied by the debt change is missing.

MediumMissing Chainlink Price Freshness ChecksFixed

The `getRationalPrice()` function of `ChainlinkOracle` fetches the latest price from a Chainlink price feed. However, the code does not check price freshness. It is best practice to check whether the returned value, `updatedAt`, is within the acceptable range of `[block.timestamp - delta, block.timestamp]` to ensure the price's freshness and detect potential off-chain oracle failures. If the oracle will be deployed on Arbitrum, it is recommended that the sequencer's uptime be checked before querying the price feed. An incorrect or stale price may be used if the sequencer is down. See [Chainlink L2 Sequencer Uptime Feeds](https://docs.chain.link/data-feeds/l2-sequencer-feeds) for more details. Additionally, Chainlink oracles have minimum and maximum prices coded in the aggregator contracts. The price update transaction will fail if the new price exceeds the range. For example, the [aggregator](https://polygonscan.com/address/0xAB594600376Ec9fD91F8e885dADF0CE036862dE0) for the MATIC/USD price feed on Polygon has a hard-coded `minAnswer` of 1000000 (0.01 USD). If MATIC prices fall below 0.01 USD, the oracle will not be updated but still return a stale price. Furthermore, the price returned by `params.dataFeed.latestRoundData()` should be validated to be greater than zero. The current check for `< 0` is not sufficient.

LowBypassing the Position Minimum Amount LimitFixed

The `positionMinAmount` parameter of a pool specifies the minimum base token amount to open a long or short position. Note that this minimum amount is a soft requirement, as users can always open a short position with a base token amount larger than the minimum amount, repay partial debt, and withdraw partial collateral from the position to reduce the position size. This can be done similarly with long positions. Note that small underwater positions may de-incentivize liquidators to liquidate them as they need to pay a larger portion of gas fees, but the pool's built-in auto-liquidation mechanism can help liquidate them.

LowMissing Input ValidationFixed

It is important to validate inputs, even if they only come from trusted addresses, to avoid human error: 1. In the `setOptions()` function of the `UniswapV3TickOracle`, `UniswapV3TickOracleDouble`, `AlgebraTickOracle`, and `AlgebraTickOracleDouble` contracts, consider adding a check to ensure `secondsAgoLiquidation <= secondsAgo` to avoid misconfiguration of the TWAP price intervals. 1. In the same function, consider enforcing a reasonable lower bound for the `secondsAgo` and `secondsAgoLiquidation` parameter.

LowPotential DoS via Empty Market ExploitFixed

In two scenarios, the coefficients in the protocol get reduced: 1. Debt coefficients may decrease when deleveraging occurs. 2. Collateral and deleverage coefficients may decrease when the liquidated position has a negative net difference. If a debt coefficient, for example, the `quoteDebtCoeff`, would ever turn zero, basically all aspects around the long positions in the pool would fully break, causing a DoS: * Longs would no longer have bad leverage if `quoteDebtCoeff = 0`, given the `positionHasBadLeverage()` function. * However, the longs could never be closed too if `quoteDebtCoeff = 0`, since the `SlippageLimit()` error should always hit, because `realQuoteDebt = 0`. * Fee accrual that longs pay shorts would also stop working, because `factor` would always be of value zero. For the `quoteDebtCoeff` to turn zero due to long-deleveraging, the following condition would need to hold: `realQuoteDebtToDeleverage = quoteDebtCoeff * discountedQuoteDebt <=> realQuoteDebtToDeleverage = realPoolQuoteDebt `, because that would result in a `quoteDebtCoeff` to be assigned a zero value in L322 during the `deleverageLong()` function. If a deleveraging requires the full pool debt to be reduced, i.e. when the only short position is fully deleveraged, the debt coefficients can turn irrevocably turn zero, causing a DoS on positions on one side of the pool. This issue is mainly relevant for newly deployed pools, as in other scenarios a full debt deleverage is extremely unlikely. However, the consequences are severe.

LowPyth Oracle Integration ImprovementsFixed

This issue describes the possible improvements on the `PythOracle` contract that may enhance its robustness and security: * `getPrice()` has a built-in check that ensures that the latest price has been updated within the last `getValidTimePeriod()` seconds. The default valid period is set to a reasonable value on each chain and is typically around 1 minute, which may not be suitable for every asset. The `getPriceNoOlderThan()` function allows the caller to adjust the time interval with the `age` parameter. * Pyth reports prices including a confidence interval. For the base price, it might be worth enforcing that the interval does not exceed a certain percentual deviation from the reported value. Additionally, it might be worth using the lower bound value for collateral price calculations and using the upper bound value for debt and leverage calculations. For more information, refer to the [Pyth Best](https://docs.pyth.network/price-feeds/best-practices#confidence-intervals) listed on Pyth's website.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (2)

  • packages/periphery/contracts/oracles/**
  • packages/contracts/contracts/**

Part of Levva Protocol’s security record

Levva Protocol
Audits2 audits by 1 firmLast audit Apr 2024, by Quantstamp.
Critical findingsNo open critical findingsAcross 2 reports.
Open Levva Protocol’s security record

Security firm

Quantstamp

236 projects reviewed · 332 reports · latest Oct 2026

See Quantstamp’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us