Levva Protocol security record

Arbitrum, Ethereum, Polygon (POS)levva.tech

As of 23 Oct 2026, Levva Protocol has 2 audit reports by 1 security firm on Trustblock; the latest is by Quantstamp, dated 25 Apr 2024. Those reports list no open critical findings.

Critical findings
No open critical findings
Across 2 reports.
Open the latest report
Your label, live
Get the label for your site

Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.

Audits2

Newest first

AuditSecurity firmDateScopeCritical findingsReport
Levva Updates and Oracle FeatureSecurity firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com
Levva ProtocolSecurity firm: QuantstampDate: Scope: Repository at commit d82fefdNo open critical findingsOpen · certificate.quantstamp.com

Findings34

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

SeverityOpenAcknowledgedFixed
Critical000
High006
Medium006
Low1912
HighIncorrect Chainlink and Pyth Oracle ImplementationLevva Updates and Oracle Feature · Quantstamp · 25 Apr 2024Fixed

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.

Open the report
HighLack of User Restrictions in Shutdown ModeLevva Protocol · Quantstamp · 11 Oct 2023Fixed

In scenarios with system insolvency, i.e., the total collateral value is insufficient to support the entire debt value on the opposite side, the protocol admin will invoke `MarginlyPool.shutdown()`. According to the [official documentation](https://docs.marginly.com/protocol-mechanics/risk-management#shutdown-mode): > In the shutdown mode, no new positions are allowed to be opened, and the side opposite to the insolvent side is responsible for restoring the system's solvency at its expense. However, according to the code implementation, users can still open new positions even if the pool is in shutdown mode, as the code does not explicitly prevent the users from doing so.

Open the report
HighMax-Heap Property Can Break when Calling `MaxBinaryHeapLib.remove()`Levva Protocol · Quantstamp · 11 Oct 2023Fixed

In the `MaxBinaryHeapLib.remove()` function, the heap replaces the to-be-removed node with the last node (i.e., the node with the largest index) and invokes `heapifyDown()` with the inserted node to adjust its position in the heap. However, the inserted node may need to be pushed toward the root by calling `heapifyUp()` instead. In such cases, the inserted node will not be placed in the correct position in the heap, which could lead to a failure in maintaining the maximum node in subsequent operations. The max heap aims to track the pool's riskiest short or long position. When the `reinit()` function is invoked, it only evaluates whether the position at the top of the heap is unhealthy, and if so, the position will get liquidated. As a result, the failure of the heap could prevent the liquidation of an unhealthy position if a healthy position is at the top of the heap. Note that liquidators can still trigger the liquidation of an unhealthy position by directly calling `receivePosition()`. However, if the liquidators are absent, the system may be unable to execute the liquidation by itself due to this bug.

Open the report
HighMax-Heap Property Can Break when Calling `receivePosition()`Levva Protocol · Quantstamp · 11 Oct 2023Fixed

In the `MarginlyPool` contract, the `receivePosition()` function allows users to receive unhealthy positions while choosing the amount of debt to repay (and the amount of collateral to add) to restore position health. This flexibility can result in changes to the position's leverage. However, since no `updateHeap()` is called at the end of the `receivePosition()` function, the `sortKey` of the user's position remains unchanged, even when the position leverage decreases. As a result, the position is incorrectly positioned in the heap, potentially preventing other unhealthy positions from being liquidated by `reinit()` due to the inconsistent sorting.

Open the report
HighUnfair Distribution of Left-over Collateral in Case of Emergency WithdrawalsLevva Protocol · Quantstamp · 11 Oct 2023Fixed

In case the collateral of the long or short position is ever exceeded by their respective debt, the pool can be shut down from its regular use and put into a so-called emergency mode by a privileged role. A short emergency, for example, happens when the base debt exceeds the quote collateral. As a result, all quote tokens of the protocol are sold off and, together with the remaining base assets of the protocol, will be withdrawable by all long positions and all base asset lenders. The withdrawable amount for the pool's leftover funds of each position is essentially proportional to the position's share of base collateral in relation to the pool's total base collateral. As leveraged long positions have a very high `discountedBaseAmount`, as their leveraged base ownership is recorded in that field, their proportional share is up to 20x (i.e. max leverage) higher than for base lenders or long positions without leverage. It is our opinion that the emergency fund distribution should be based on the collateral deposited to the protocol and not based on possibly leveraged amounts. The fairness is also not the only concern here. The emergency mode activation e.g. a short emergency could be front-run by a max leverage long position that sells off all remaining quote collateral (without the `MarginlyParams.quoteLimit` being exceeded for the base token) that could manage to get a great share of the whole protocol's funds that way. It should also be mentioned that due to all quote collateral being sold off and made accessible to the base lenders and longers, even the base lenders or the unleveraged longs could profit from such emergencies (specifically if `emergencyCoefficient > 1`).

Open the report
HighVulnerability to Sandwich Attacks Due to Ineffective Slippage ChecksLevva Protocol · Quantstamp · 11 Oct 2023Fixed

Marginly pools execute swaps through the router when a user creates a long or short position, closes their position, or their position is liquidated. The pool calculates a minimum output or maximum input token amount based on the spot price of a Uniswap V3 pool. The router then enforces that the actual received or input amount falls within the limits. However, slippage checks based on spot prices of Uniswap V3 pools may be ineffective, as the price impact of a user’s trade can be relatively small when the pool has enough liquidity. In other words, the larger the pool’s liquidity, the less significant the price impact tends to be. This ineffectiveness is exacerbated by Uniswap V3’s centralized liquidity design, where specific tick ranges can have particularly deep liquidity, further reducing the potential price impact of the user’s trade. As the slippage checks are ineffective in protecting users from significant price changes, the user may suffer from sandwich attacks. In such attacks, the attacker manipulates token balances in a Uniswap V3 pool, thereby influencing the outcomes of swaps. As a result, users may receive fewer output tokens or may be required to transfer more input tokens to the pool than expected. The possible consequences in each scenario are: - Open position: Users might have a position with a higher leverage level than intended. - Close position: Users may receive fewer tokens than expected. - Liquidation: The swapped amount may fall short of covering the debt, causing the lenders to absorb the losses instead of resulting in profit.

Open the report
MediumInaccurate Interest CalculationLevva Updates and Oracle Feature · Quantstamp · 25 Apr 2024Fixed

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.

Open the report
MediumIncorrect Accounting in `receivePosition()`Levva Updates and Oracle Feature · Quantstamp · 25 Apr 2024Fixed

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.

Open the report
MediumMissing Chainlink Price Freshness ChecksLevva Updates and Oracle Feature · Quantstamp · 25 Apr 2024Fixed

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.

Open the report
MediumDenial-of-Service via Massive Collateral DepositsLevva Protocol · Quantstamp · 11 Oct 2023Fixed

If a user wants to keep their leveraged long or short position from being liquidated, they might need to deposit additional collateral to lower the position's leverage. Before the collateral deposit is accepted, it is checked that the pool's total collateral value (denominated in the quote asset price), together with this deposit, does not exceed `MarginlyParams.quoteLimit`. Else, the transaction reverts. This opens up the possibility of a DoS-attack vector, wherein a malicious actor possessing substantial financial resources strategically deposits both the quote and base assets as a lending position into the protocol with the goal of elevating the protocol's asset balances for both the quote and base assets to reach `MarginlyParams.quoteLimit`. Unless profitable positions are closed, or liquidations occur, these values will not change, making the active position's leverage unadjustable. As most liquidations absorbed by the pool will profit collateral holders, there is an incentive for malicious actors to perform such an attack. This could also happen in more natural, non-malignant conditions: With a falling base asset price, many shorts would close, increasing the base balance in the protocol. If such closing positions reach the quote limit, long positions cannot keep their positions healthy.

Open the report
Show 10 more
MediumLack of Slippage Control in Emergency ShutdownLevva Protocol · Quantstamp · 11 Oct 2023Fixed

In the event of a short emergency within a Marginly ETH/USDC pool, where the total ETH debt value exceeds the total USDC collateral value, the protocol admin initiates a pool shutdown and sets the mode to `ShortEmergency` by invoking the `setEmergencyMode()` function. During the execution of this function, all USDC collateral minus the USDC debt is swapped into ETH using the Marginly router. However, the swap is executed with the minimum output token amount set to 0, which makes the swap susceptible to sandwich attacks. In the most severe scenario, the pool may receive nearly 0 ETH in return, with most of the value extracted by the malicious attacker.

Open the report
MediumSystem Leverage Can Briefly Exceed Max LeverageLevva Protocol · Quantstamp · 11 Oct 2023Fixed

The Marginly Pool can hold multiple to-be-liquidated positions at once. If protocol usage is slow and bots are not participating in liquidations via `receivePosition()`, the total debt of one position's side can, therefore, briefly exceed the collateral of a position by a factor greater than the max leverage. This is especially note-worthy in situations where the pool would be about to shut down, i.e., where the base debt exceeds the quote collateral. If such a shutdown call would be front-run by a user calling `receivePosition()` on a few liquidatable short positions (i.e., increasing quote collateral), they could perform enough liquidations to stop the short-emergency from being activated. In such a scenario, the system leverage could be almost arbitrarily high (assuming, e.g., base debt is equal to a hundred thousand USD and quote collateral equal to a hundred thousand and one USD would update the system short leverage to a hundred thousand), making the interest rate for shorts for the period possibly vastly higher than anticipated. The conditions for this would, however, be improbable to happen. Not only would the pool need to be close to an emergency state, but an attacker would need to be perfectly organized to profit from such an attack (i.e., have a massive long position in place to receive the fees and front-run the emergency activation with a perfectly orchestrated series of `receivePosition()` calls).

Open the report
LowBypassing the Position Minimum Amount LimitLevva Updates and Oracle Feature · Quantstamp · 25 Apr 2024Fixed

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.

Open the report
LowMissing Input ValidationLevva Updates and Oracle Feature · Quantstamp · 25 Apr 2024Fixed

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.

Open the report
LowPotential DoS via Empty Market ExploitLevva Updates and Oracle Feature · Quantstamp · 25 Apr 2024Fixed

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.

Open the report
LowPyth Oracle Integration ImprovementsLevva Updates and Oracle Feature · Quantstamp · 25 Apr 2024Fixed

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.

Open the report
LowApplication Monitoring Can Be Improved by Emitting More EventsLevva Protocol · Quantstamp · 11 Oct 2023Fixed

Emitting events is a good practice to validate the proper deployment and initialization of the contracts. Also, any important state transitions can be logged, which is beneficial for monitoring the contract and tracking eventual bugs or hacks. Below, we present a non-exhaustive list of events that could be emitted to improve application management: - `FullMarginlyFactory.changeSwapRouter()` should emit the new swap router address. - `MarginlyFactory.changeSwapRouter()` should emit the new swap router address. - `MarginlyPool.setParameters()` should emit the global pool parameters. - `RouterStorage.addDexAdapter()` should emit the address of the DEX adapter added. - `AdapterStorage.addPools()` should emit the pool address added.

Open the report
LowClone-and-OwnLevva Protocol · Quantstamp · 11 Oct 2023Fixed

The clone-and-own approach involves copying and adjusting open-source code at one's discretion. From the development perspective, it is initially beneficial as it reduces the amount of effort. However, from the security perspective, it involves some risks as the code may not follow the best practices, contain a security vulnerability, or include intentionally or unintentionally modified upstream libraries. `OracleLib.sol` is copied, and its Solidity version is upgraded to `0.8.x`, allowing it to be used with the other contracts. Also, an `unchecked` block is added to the `getSqrtRatioAtTick()` function, as the arithmetic operations were performed without an overflow check in the previous Solidity version.

Open the report
LowCritical Role Transfer Not Following Two-Step PatternLevva Protocol · Quantstamp · 11 Oct 2023Fixed

The owner of the factory contract can call `setOwner()`, and the owner of the `AdapterStorage` and `RouterStorage` can call `transferOwnership()` to transfer the ownership to a new address. Suppose an uncontrollable address is accidentally provided as the new owner's address. In that case, the contract will no longer have an active owner, and functions restricted to the contract owner can no longer be executed. - `FullMarginlyFactory` and `MarginlyFactory` have a custom owner implementation and use `setOwner()` to transfer the ownership, which does not follow the two-step pattern. - `AdapterStorage` and `RouterStorage` inherit OpenZeppelin's `Ownable` contract.

Open the report
LowMissing Input ValidationLevva Protocol · Quantstamp · 11 Oct 2023Fixed

It is important to validate inputs, even if they only come from trusted addresses, to avoid human error. Specifically, in the following functions, arguments of type `address` may be initialized with value `0x0`: - `FullMarginlyFactory.constructor()`: - `uniswapFactory` - `swapRouter` - `feeHolder` - `WETH9` - `techPositionOwner` - `FullMarginlyFactory.setOwner()` - `owner` - `MarginlyPool._initializeMarginlyPool()` - `quoteToken` - `baseToken` - `quoteTokenIsToken0` - `uniswapPool` - `params` - `MarginlyPool.setParameters()` - `params`

Open the report
Show 4 more
LowOwner Can Set Arbitrary FeesLevva Protocol · Quantstamp · 11 Oct 2023Fixed

The owner can set arbitrary fees via the function `MarginlyPool.setParameters()`. Specifically, the owner can set the following fees and rates up to and exceeding 100%. - `MarginlyPool.params.interestRate` can be set above 100% (1,000,000). - `MarginlyPool.params.fee` can be set above 100% (1,000,000). - `MarginlyPool.params.swapFee` can be set above 100% (1,000,000). - `MarginlyPool.params.positionSlippage` can be set above 100% (1,000,000). - `MarginlyPool.params.mcSlippage` can be set above 100% (1,000,000).

Open the report
LowOwnership Can Be RenouncedLevva Protocol · Quantstamp · 11 Oct 2023Fixed

If the owner renounces ownership, all ownable contracts will be left without an owner. Consequently, any function guarded by the `onlyOwner` modifier will no longer be able to be executed. The functions listed under _Privileged Roles and Ownership_ will no longer be executable if contract ownership is renounced.

Open the report
LowThe Use of `SafeERC20.safeApprove()`Levva Protocol · Quantstamp · 11 Oct 2023Fixed

The code relies on the function `SafeERC20.safeApprove()`. This function has been deprecated and shall not be used instead of `approve()` since 1) it can still be front-run, 2) it uses additional gas, and 3) it does not work with tokens that are not ERC-20 compliant. For more information, see [this issue](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2219). - `MarginlyKeeper.liquidateAndTakeProfit()` uses `IERC20.approve()` and `SafeERC20.safeApprove()`, which is generally not recommended. - The other files use Uniswap's `TransferHelper.safeApprove()`, which still has the issue of being unable to change a currently active approval. Some places in the code call `TransferHelper.safeApprove(0)` to solve this issue, but some don't.

Open the report
LowUnlocked PragmaLevva Protocol · Quantstamp · 11 Oct 2023Fixed

Every Solidity file specifies a version number of the format `pragma solidity (^)0.8.*` in the header. The caret (`^`) before the version number implies an unlocked pragma, meaning the compiler will use the specified version _and above_, hence the term "unlocked".

Open the report

Contracts

No deployed address is recorded for these audits.

The reports name a repository or files. Open a report to see its scope.

The repositories and files the reports name

Show this on your site

Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.

Content

Facts

Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.

Fixed as
Language

Style

Layout
Theme
Accent
Corners

Format

Size
Date
Embed as
HTML
<head>
<script src="https://widget.trustblock.run/project-widget.min.js" defer></script>
</head>
<body>
<div
data-tb-project-widget
data-tb-project-slug="levva-protocol"
></div>
</body>

Paste the code where the label should appear. It uses no API credit and updates by itself.

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