Quantstamp reviewed Levva Protocol (Levva Updates and Oracle Feature) on 25 Apr 2024: 10 findings, 0 critical; 8 marked fixed, 1 acknowledged, 1 open.
Report hosted on certificate.quantstamp.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 1 |
| Medium | 0 | 0 | 3 |
| Low | 1 | 1 | 4 |
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.
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.
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.
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.
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.
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.
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.
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.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (2)
236 projects reviewed · 332 reports · latest Oct 2026
See Quantstamp’s auditsSomething 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