Smardex engaged Guardian to review the security of its review of their decentralized synthetic dollar. From the 30th of September to the 4th of November, a team of 7 auditors reviewed the source code in scope.
- Published
- Review window
- September 30 to November 4, 2024
- Language
- Solidity
- Chains
- Ethereum
- Sector
- DEXs and AMMs, Stablecoins
- 2 Critical
- 2 High
- 34 Medium
- 79 Low
- 0 Informational
Scope
Overview
Smardex engaged Guardian to review the security of its review of their decentralized synthetic dollar. From the 30th of September to the 4th of November, a team of 7 auditors reviewed the source code in scope.
Issues Detected Throughout the engagement 4 High/Critical issues were uncovered and promptly remediated by the Smardex team. Several issues impacted the fundamental behavior of the protocol, following their remediation Guardian believes the protocol to uphold the functionality described for the USDN protocol.
Findings 117
-
C-01 Critical Rebalancer Position Closed Twice Logical Error Resolved
Description
Proof of concept: PoC
When initiating a close order for the rebalancer position with the
initiateClosePositionfunction it is possible that the initiation of the close triggers the rebalancer itself.This results in many issues, including but not limited to:
- Double counting the balance which is being closed
- Removing the closed amount from the rebalancer's previous tick twice
- Overwriting the state updates which were made in the
Rebalancer.updatePositionfunction
Recommendation
Revert the
Rebalancer.initiateClosePositionfunction when a rebalancer is triggered by the_usdnProtocol.initiateClosePositionfunction call.Resolution
Smardex Team: The issue was resolved in PR#627.
-
C-02 Critical Validate Withdraw Uses Current Total Shares Logical Error Resolved
Description
Proof of concept: PoC
The
_validateWithdrawalWithActionfunction does not use thetotalSharesamount saved at init in the pending action. Instead, it calculates the amount of assets a user should receive by using the currenttotalSharesamount, together with the availablebalanceVaultfrom init.Therefore reducing the
totalSharesamount between init and validation of a withdrawal will give the user more assets than the user should receive and vice versa.This can be exploited in the following way:
- Attacker deposits funds
- Attacker initiates multiple withdrawals of part of his shares for example two withdrawals of 50% of
his shares
- The first withdrawal is validated and reduces the
totalSharesamount - The second withdrawal is validated and uses the
balanceVaultfrom init but the reducedtotalShares
amount to calculate how many assets the attacker receives and therefore sends the attacker more assets than he should receive
- Attacker repeats the process to drain the protocol
All of this is independent of price changes allowing the attack to be performed swiftly and consistently.
Recommendation
Use the
totalSharesamount saved at init in the pending action instead of the current one.Resolution
Smardex Team: The issue was resolved in PR#638.
-
H-01 High Users Do Not Pay Funding During Initiation Logical Error Resolved
Description
Proof of concept: PoC
Upon initiation, users positions are effectively created and they have an effect on the skew in exposure between the vault and traders, thus the funding is accounting for this initiated position. However the position does not pay the funding during the period from
[initiateOpen, validateOpen]as the new position exposure is computed with the new liquidation price, accounting for the updates made to the liquidation multiplier.The vault and long balances are corrected with the
_validateOpenPositionUpdateBalancesfunction upon validation so that the funding which was not actually charged to the user is not included in the aggregate balance accounting, therefore this avoids a fundamental accounting error. However there are two issues which stem from this behavior.Firstly, the user who initiated their position does not have to pay their funding fees while they have still manipulated the skew. Thus potentially making other traders pay funding while not having to pay funding themselves. A malicious actor could initiate a large long position and wait to validate it as long as they can before immediately initiating and validating a close action to affect the funding skew as much as possible while paying as little as possible themselves.
And secondly, the trader is immune from the funding fees which take place in the timeframe between
[startPriceTimestamp, lastPriceTimestamp]. Technically the trader should experience these funding fees as their position is not opened at the lastPrice, but at thestartPricewhich can be from an earlier timestamp.Recommendation
Consider charging the position the funding fees during the period:
[initiateOpen, validateOpen]. This way traders are held accountable for the affect they have on the skew, and they will pay the funding fees they rightfully should from the period[startPriceTimestamp, lastPriceTimestamp].The downside of this approach is that the trader is charged/credited with funding in the range
[initiateOpen,startPriceOnValidation.timestamp]which they technically shouldn’t be since their position is not officially opened and exposed to this price action. However this range should be minimal given that the oracle middleware aims to use the earliest price in the range from[initiateOpen + 24 seconds, validateOpen]to execute validations at.This could be implemented by using a liquidation price based upon the
liquidationMultiplierrecorded at initiation for the new position exposure calculation. Additionally, the new value of the position should be based upon a liquidation price with the latestliquidationMultiplier. In the_validateOpenPositionWithActionfunction when computing theexpoAfterfor the normal validation case, where leverage does not exceed the max:uint128 liqPriceWithoutPenaltyWithoutFunding = Utils._getEffectivePriceForTick(Utils.calcTickWithoutPenalty (data.action.tick, data.liquidationPenalty), + data.action.liqMultiplier +); // calculate the new total expo uint128 expoBefore = data.pos.totalExpo; • uint128 expoAfter = Utils._calcPositionTotalExpo(data.pos.amount, data.startPrice, data.liqPriceWithoutPenalty); + uint128 expoAfter = Utils._calcPositionTotalExpo(data.pos.amount, data.startPrice, liqPriceWithoutPenaltyWithoutFunding);Resolution
Smardex Team: The issue was resolved in PR#628.
-
H-02 High Liquidation Rewards Sent To The Rebalancer Will Be Lost Unexpected Behavior Resolved
Description
The
Rebalancercontract implements the functioninitiateClosePositionwhich is responsible for closing a portion of the currentRebalancerposition within theUsdnProtocol, based on a user's deposit.This function makes an external call to
_usdnProtocol.initiateClosePosition. During this external call, where theRebalancercontract acts as the caller, one or more liquidity ticks may be liquidated. If liquidation occurs, theRebalancercontract will receivewstETHtokens as a reward.However, these
wstETHtokens are not forwarded to the original caller ofrebalancer.initiateClosePositionand instead remain trapped within theRebalancercontract, where they will be permanently inaccessible.Recommendation
Consider tracking the
wstETHbalance of theRebalancercontract before and after the_usdnProtocol.initiateClosePositioncall. If thewstETHbalance was increased after the call, send the difference ofwstETHtokens to therebalancer.initiateClosePositioncaller.Resolution
Smardex Team: The issue was resolved in PR#633.
-
M-01 Medium Imbalance Does Not Count Funding Logical Error Acknowledged
Description
While initiating a request to open a position the
tradingExpoused to compute the new positions predicted exposure is based on alongTradingExpoWithFundingwhich includes the funding up until the currentblock.timestamp.However the imbalance validations that follow, and the imbalance validations throughout the codebase use the current vault and long balances which ignore the funding accrued in the timeframe from the
lastUpdateTimeuntil the currentblock.timestamp.Therefore there is an immediate contradiction in the imbalance validation during the initiation of an update request. The position exposure includes the latest funding, while the aggregate balances do not.
Furthermore, the imbalance validations for all actions cannot be accurately validated against the latest funding changes which have yet to be stored.
Recommendation
Consider accounting for the funding at the latest timestamp in the imbalance validation functions throughout the codebase. Furthermore, consider if the latest unrecorded funding should be taken into account when triggering the rebalancer.
Resolution
Smardex Team: Acknowledged.
-
M-02 Medium Positions Opened Above Max Leverage MEV Acknowledged
Description
When validating the creation of a position if the new leverage is over the
maxLeveragethen the position is adjusted to be at or within themaxLeveragebound. First, thecurrentLiqPenaltyand theliquidationPricecorresponding tomaxLeverageis used to select the liquidation tick for the position.Then if the
liquidationPenaltyis different on the selected tick, that new penalty is applied to thedata.liqPriceWithoutPenaltywhich is ultimately used to determine the leverage of the position.However when the liquidation tick with penalty has a lower
liquidationPenaltythan thecurrentLiqPenaltyin storage than the resultingliqPriceWithoutPenaltycan be higher than the originalliqPriceWithoutPenaltywhich was based on themaxLeverage.maxLeverage = startPrice / (startPrice - liqPriceWithoutPenalty0)Then, sinceliqPriceWithoutPenalty0 < liqPriceWithoutPenalty1:newLeverage = startPrice / (startPrice - liqPriceWithoutPenalty1) > maxLeverageThis is unexpected for the protocol as no positions should have a leverage greater than the
maxLeverage. Additionally, it is worth noting that the same can occur for the rebalancer position when determining theliqPriceWithoutPenaltyin the_calcRebalancerPositionTickfunction.Recommendation
Consider if it is acceptable to have positions which exceed the
maxLeverage. The most straightforward solution would be to configure themaxLeverageaccordingly to account for the fact that some positions may go slightly over it depending on theliquidationPenaltyupdates. Be sure to also keep this in mind when updating thecurrentLiqPenaltyvalue in storage.Resolution
Smardex Team: Acknowledged.
-
M-03 Medium Bad Debt Value Extraction Documentation Resolved
Description
Proof of concept: PoC
When positions with bad debt are liquidated on a tick where the liquidation price without penalty is above the current price, then a correction is made to the balance of the vault.
The losses which are in excess of the position's collateral were errantly counted as going towards the vault. Upon liquidation these errant trader losses are clawed back from the vault.
Depositors may observe that this correction is going to occur if a liquidation round occurs and intentionally frontrun this to initiate a withdrawal.
The initiation of a withdrawal will protect the depositor from this clawing back of inaccurate trader losses because the
balanceVaultandbalanceLongare cached on the withdrawal object.Furthermore this clawing back of bad debt can cause a stepwise decrease in the price of USDN which may also be arbitrageable by shorting USDN.
Recommendation
Be aware of this risk of late liquidations and carefully consider it when configuring liquidation rewards and liquidation penalties.
Resolution
Smardex Team: The issue was resolved in PR#722.
-
M-04 Medium pendingBalanceVault Underflow Rounding Resolved
Description
The
pendingBalanceVaultis deducted by the anticipated withdrawal amount when a withdrawal action is initiated.However due to extreme price action in some edge cases the actual balance of the vault may fall below the pending balance, in which case this would cause an underflow revert in all areas where the pending vault balance is added to the vault balance.
This would effectively block the queue and shut down the protocol.
Recommendation
It may not be deemed too complex to handle this edge case in all locations for this iteration of the protocol. However be aware of this edge case and consider handling it.
Resolution
Smardex Team: The issue was resolved in PR#711.
-
M-05 Medium Initiators Avoid A Portion Of Funding Logical Error Acknowledged
Description
Proof of concept: PoC
When initiating a position the user’s exposure is computed accounting for the current funding rate accrued to the latest
block.timestamp.However upon initiation users can affect the funding directly after the initiation is complete in the following time range
[initiationLastUpdateTimestamp, initiationBlockTimestamp]as long as a subsequent action is performed with another price before or near theinitiationLastUpdateTimestamp.This way users who initiate do not have to incur the cost of the existing funding rate over the range
[initiationLastUpdateTimestamp, initiationBlockTimestamp], meanwhile their positions are affecting the skew and thus should incur the full funding cost.These positions will incur the difference in the funding rate that they cause, but not the base funding rate that was pre-existing before they initiated their position.
If positions are not held accountable for the total funding rate in the range
[initiateLatestPriceTimestamp, initiateBlockTimestamp], then they can force other long positions to pay for increased funding, by way of increasing the long exposure, while not paying for it themselves.This may allow for extractable value for a USDN depositor or cause other positions to be liquidateable by this manipulation.
Recommendation
Consider only using the long exposure without syncing the funding to the latest timestamp, e.g.
UsdnProtocolCoreLibrary.longTradingExpoWithFunding(s, lastPrice,uint128(s._lastUpdateTimestamp)). Or simply,s._totalExpo - s._longBalance.Resolution
Smardex Team: Acknowledged.
-
M-06 Medium _triggerRebalance Does Not Account For Liquidation Rewards Unexpected Behavior Acknowledged
Description
The
_triggerRebalancefunction is responsible for initiating a rebalance when the imbalance on the long side becomes too significant, aiming to restore the protocol to a balanced state. It calculates the tick of the rebalancer position to open using the_calcRebalancerPositionTickfunction. However, this function relies on an outdated value ofcache.vaultBalance, as the actuals._balanceVaultstate variable is reduced in the_sendRewardsToLiquidatorfunction.By ignoring the
s._balanceVaultdecrease caused by the liquidation rewards, it is possible that after the rewards are paid out and removed from thes._balanceVaultthe protocol enters again an unbalanced state especially if thes._balanceVault's liquidity is low. Although the maximum liquidation rewards are currently capped at 0.5 Ether, and the protocol is expected to hold significantly more in its vault, this value could still be updated by a privileged account to a higher one.Finally, the
_usdnRebasefunction is also affected by this as it uses thes._balanceVaultto calculate the USDN price. This calculation will not be accurate as thes._balanceVaulthas not been updated/decreased by the time_usdnRebasefunction is called.Recommendation
Consider revising the calculation of liquidation rewards by dividing it into two components:
- Fixed component: A fixed portion of the liquidation reward based on the number of ticks
liquidated. This component should be slightly optimistic, providing a small excess to account for potential future events such as a
triggerRebaseorusdnRebase. 2. Variable component: A variable portion of the liquidation reward, calculated based on the remaining collateral after liquidation.This way, the exact liquidation reward will be known beforehand and the
_triggerRebalanceand_usdnRebasefunctions can account for the exacts._balanceVaultdecrease caused by the liquidation rewards distribution.Resolution
Smardex Team: Acknowledged.
-
M-07 Medium Slippage Check Occurs Before Adjustment Validation Resolved
Description
In the
_prepareInitiateOpenPositionDatafunction theparams.userMaxPriceis validated against thelastPrice, however the user’s execution price will include a fee which is not included in thelastPrice.On the following line the
data_.adjustedPriceis assigned which is adjusted by this fee and more closely estimates the execution price that the user will experience.Ideally the fee is included in the slippage validation since it will affect the user’s ultimate execution price, and allows users to protect themselves if the
_positionFeeBpswere to be unexpectedly changed.Recommendation
Consider validating the
params.userMaxPriceagainst theadjustedPricewhich includes the position fee.Resolution
Smardex Team: The issue was resolved in PR#613.
-
M-08 Medium removeBlockedPendingAction Extractable Value MEV Resolved
Description
In the
_removeBlockedPendingActionfunction when aValidateClosePositionpending action is removed the entirecloseBoundedPositionValueamount is added to the vault.This means the trader loses their entire position value in the event that their close position action is stuck, even if they were nowhere near close to liquidation. This could cause significant loss for a trader which has a large position.
Furthermore, this creates a potentially large arbitrage opportunity. If a removal of a
ValidateClosePositionpending action for a large position close is sitting in the mempool it would be potentially significantly profitable for an actor to front-run this transaction and initiate a deposit into the vault right before the removal takes place.Because vault deposits only vary based on price action between initiation and validation the malicious actor would realize their corresponding value of the immediate vault balance increase from the removal of the pending close position action.
Recommendation
Instead of giving the entire position value to the vault upon removal of a close pending action, consider sending the position value to the specified
toaddress. The protocol can then manually decide how much should be refunded to the trader versus donated to the vault via a new donate function.Resolution
Smardex Team: The issue was resolved in PR#672.
-
M-09 Medium Tick Liquidation Penalty Cannot Be Reset Unexpected Behavior Resolved
Description
In the
_removeAmountFromPositionfunction the liquidationPenalty is not reset on the tick data after all positions have been removed.Additionally, during the initiate open flow, the existing tick's liquidation penalty as the result of
getTickLiquidationPenalty(s, data_.posId.tick)is used to re-assign the new liquidation penalty.Thus even after all positions have been cleared from the tick, the latest configured liquidation penalty cannot be assigned to this tick.
Recommendation
Reset the tick's
liquidationPenaltyto zero after all positions have been removed from the tick in the_removeAmountFromPositionfunction.Resolution
Smardex Team: The issue was resolved in PR#615.
-
M-10 Medium Incorrect pendingBalanceVault Correction Logical Error Resolved
Description
In the
_removeBlockedPendingActionfunction the_pendingBalanceVaultis corrected by computing the withdrawal amount without accounting for fees.However upon the initiation of the withdrawal the
_pendingBalanceVaultwas decremented by the withdrawal amount less fees.As a result when a withdrawal action is removed with the
_removeBlockedPendingActionfunction usingcleanupastruethe_pendingBalanceVaultexperiences an invalid net increase by the withdrawal fee amount.Recommendation
Correct the
_pendingBalanceVaultby the withdrawal amount accounting for fees in the_removeBlockedPendingActionfunction.Resolution
Smardex Team: The issue was resolved in PR#657.
-
M-11 Medium _calcPositionSizeBonus Errant Token Amount Logical Error Resolved
Description
The position size bonus for liquidator remuneration is computed with the
_calcPositionSizeBonusfunction.The bonus is based upon a difference in the current price and the liquidated ticks price and the documentation for the
_calcPositionSizeBonusindicates that the resulting value is denominated in native ether.However when the asset is wstEth and the
WstEthOracleMiddlewareis used the price will be for wstEth and thus the_calcPositionSizeBonusfunction will return a wstEth amount.However this returned value is treated as native ether and converted to wstEth redundantly on line 108:
wstETHRewards_ = _wstEth.getWstETHByStETH(totalRewardETH);Recommendation
Instead of converting the position size bonus amount to wstEth with the
getWstETHByStETH(totalRewardETH)call, add it to the resulting wstEth value. If this approach is taken be sure to cap themaxRewardto the resulting wstEth amount and assign this number as a wstEth value.Resolution
Smardex Team: The issue was resolved in PR#632.
-
M-12 Medium Inaccurate Imbalance Check In _checkImbalanceLimitOpen Validation Resolved
Description
The function
_checkImbalanceLimitOpeninaccurately calculates the imbalance when initiating the opening of a new position because it does not account for the position fees deducted. This miscalculation could allow the system to enter a state where it is unbalanced beyond what should be possible after the action is validated.This is because
_checkImbalanceLimitOpencalculates theimbalanceBpsby assuming that the balance of the vault is equal to justs._balanceVault + s._pendingBalanceVaultand that the balance of the long side is equal tos._balanceLong + openCollatValuewhereopenCollatValueis equal toparams.amount.However,
openCollatValuedoes not reflect the true position value and should be instead equal todata_.positionValue(params.amountminus the position fee paid). On the other hand,currentVaultExposhould also include the position fee which can be calculated asparams.amount - data_.positionValue. Do notice that this update is performed after_checkImbalanceLimitOpencheck.Recommendation
Consider updating the imbalance check to include the fees.
Resolution
Smardex Team: The issue was resolved in PR#616.
-
M-13 Medium Exposure During Closure Unaccounted Logical Error Acknowledged
Description
During the initiation of a close action, the position, or the portion of a position being removed, is effectively removed from the protocol as it is deducted from it’s corresponding tick and the cumulative balance and exposure accounting. However the USDN vault still serves as a counterparty for any PnL realized by the position in the period: [
initiateClose,validateClose] and the user is still exposed to this price action as well. This may present an issue as the system will technically be more exposed to the long side than the imbalance tracking accounts for. As a result the corresponding imbalance mechanisms may not be used when they ought to.Extracting value from a vault withdrawal (assuming 0 protocol fees): 1. Bob initiates close position. Bob provided empty Pyth data so
Pyth.getPriceUnsafe()is called retrieving the price of Ether 59 minutes ago (price manually updated on-chain by Bob 59 minutes ago). The price retrieved is 1750 even though the current Ether price is 59 minutes later/now 1925. 14 minutes passes and Bob has not validated yet. 2. Alice initiates a withdrawal providing the actual current Pyth price of (1925 Ether price). We are assuming a 10% increment of Ether price in 1 hour 14 minutes. 3. Bob validates its position closure, andbalanceVaultis decreased based on the size of the long position closed and on the 10% price increase. 4. Alice validates withdrawal, but she still gets the assets respective to thevaultBalanceat the time of the initiation. As thevaultBalancewas higher, Alice UNFAIRLY gets more assets than she deserves. As at minute 60 + 14 the position value of Bob was already determined (it was already determined at minute 60 + 24 seconds) but Bob decided to delay his validation and because of this Alice received a higher amount of assets.assetsReceivedWithExploit ................................... 1821845730818816586 assetsReceivedWithoutExploit ................................ 1813360204002626022 Increment of ................................................ 46794/10000000 (0.47%) Value of the position closed compared to total long value ... 2% Price change ................................................ 10% increment of Ether priceExtracting value from a vault deposit (assuming 0 protocol fees): 1. Bob initiates close position. Bob provided empty Pyth data so
Pyth.getPriceUnsafe()is called retrieving the price of Ether 59 minutes ago (price manually updated onchain by Bob 59 minutes ago). The price retrieved is 1750 even though the current Ether price is 59 minutes later/now 1575. 14 minutes passes and Bob has not validated yet. 2. Alice initiates a deposit providing the actual current Pyth price of (1575 Ether price). We are assuming a 10% decrement of Ether price in 1 hour 14 minutes. 3. Bob validates its position closure, andbalanceVaultis increased based on the size of the long position closed and on the 10% price decrease. 4. Alice validates her deposit, but she still gets the shares respective to thevaultBalanceat the time of the initiation. As thevaultBalancewas lower, Alice unfairly gets more shares than she deserves. As at minute 60 + 14 the position value of Bob was already determined (it was already determined at minute 60 + 24 seconds) but Bob decided to delay his validation and because of this Alice received a higher amount of shares.sharesReceivedWithExploit ................................... 3621365498082123194456875654090209456802 sharesReceivedWithoutExploit ................................ 3604753424386868175628340801033648314777 Increment of ................................................ 45872/10000000 (0.46%) Value of the position closed compared to total long value ... 2% Price change ................................................ 10% decrement of Ether priceRecommendation
Consider introducing specific accounting in the imbalance calculations for the exposure of all position amounts which have been effectively closed after initiation, but have yet to be validated and have their exposure officially removed. If this approach is taken, then one inconsistency should be addressed. When an individual liquidation occurs during a close action validation, the position is valued at the block in which the action was initiated, however this contradicts the logic that follows for a normal close, where the vault is exposed to the current value of the position.
These two scenarios should be standardized in terms of exposure. If the first suggestion is implemented, the liquidations should be adjusted to also have exposure to the latest asset price. Alternatively, the exposure over the period [
initiateClose,validateClose] could be removed entirely, and the exact position value to be realized could be computed during the initiation.Resolution
Smardex Team: Acknowledged.
-
M-14 Medium Innacurate isLiquidationPending Check In ValidateOpen Validation Resolved
Description
The
_prepareValidateOpenPositionDatafunction implements the following check:data_.liqPriceWithoutPenalty = Utils.getEffectivePriceForTick (s, Utils.calcTickWithoutPenalty(data_.action.tick, data_.liquidationPenalty)); data_.lastPrice = s._lastPrice; if (data_.lastPrice < data_.liqPriceWithoutPenalty) {// the position must be liquidated data_.isLiquidationPending = true; return (data_, false);}This check compares the
s._lastPriceto the liquidation price without the penalty. If thes._lastPriceis lower, the function marks the position as liquidatable and halts further validation. However, this check is not accurate because the liquidation price should factor in the liquidation penalty.By only comparing against the liquidation price without the penalty, when there is a
lastPricebetween theliqPriceWithoutPenaltyand the actual liquidation price (including the penalty), position is not being flagged for liquidation, even though it should be.Recommendation
To fix this, the liquidation check should compare the
s._lastPriceagainst the liquidation price with the penalty. The updated check should look like this:uint256 liqPrice = Utils.getEffectivePriceForTick(s, data_.posId.tick); if (data_.lastPrice < liqPrice) {// the position must be liquidated data_.isLiquidationPending = true; return (data_, false);}Resolution
Smardex Team: The issue was resolved in PR#631.
-
M-15 Medium Fee Change Hurts Traders Logical Error Acknowledged
Description
In the
_prepareValidateOpenPositionDatafunction thestartPriceis affected by the current fee in storage, however this fee may be different than the one that was applied upon initiation and validated against theuserMaxPrice.This is in contrast to the deposit and withdrawal flow, where the
vaultFeeBpsare cached onto the deposit/withdrawal object and used during validation.Recommendation
Consider caching the
_positionFeeBpsvalue upon initiation in the open position object to be applied on validation.Resolution
Smardex Team: Acknowledged.
-
M-16 Medium Min Price Validation Excludes Fee Validation Resolved
Description
In the
_prepareClosePositionDatafunction theuserMinPriceis validated against thelastPrice, however the price applied to the value of the position and thus the amount received is reduced by the position_positionFeeBps.Thus the resulting price experienced by the user upon closing their position can often be less than the minimum desired without any unpredictable price action occurring between the initiation and validation of a close.
The position fees are easy to predict ahead of time, and thus should be included in the
userMinPricevalidation.Recommendation
Consider reducing the
lastPriceby the_positionFeeBpswhen validating theuserMinPricein the_prepareClosePositionDatafunction.Resolution
Smardex Team: The issue was resolved in PR#630.
-
M-17 Medium Risk Free Trades With Rebalancer Gaming Resolved
Description
Proof of concept: PoC
The Rebalancer is allowed to open positions at the current
lastPriceimmediately, without going through the initiation and validation process. This opens up the opportunity for risk free trades for the users who deposit into the rebalancer.There there are several mechanisms in the Rebalancer to prevent the gameability of a risk free trade, but it is still possible to extract risk free value from the Rebalancer mechanism. The goal of an actor in this exploit is to get their deposit included in the rebalancer position to take advantage of an outdated
lastPriceupon the triggering of the rebalancer.A malicious actor can see when a large position or a large set of positions are close to being liquidated and initiate a deposit into the rebalancer. If the rebalancer is able to be triggered with a
lastPricethat is less than the current market price within the[initiateRebalancerDeposit,initiateRebalancerDeposit + 24 seconds]window, then the user can immediately game the stale pricing by:- Validating their rebalancer deposit
- triggering the rebalancer via a liquidation call
- Exiting the rebalancer with the
initiateClosePositionfunction
If the correct conditions are not met within the timeframe, the actor can simply choose to not validate their deposit and wait until the cooldown period has ended to collect their funds. The actor can open consecutive initiate deposits with multiple addresses to ensure that they are able to take advantage of a rebalancer triggering in a given timeframe.
Recommendation
The extractable value from this grows with the size being liquidated and the imbalance created, however it is unlikely to occur with a great magnitude consistently. Therefore it may be fine to acknowledge this extractable value. Otherwise consider taking further measures to reduce the feasibility of this value extraction.
Such as introducing a fee upon validation or cancellation of a rebalancer deposit, or allowing other users to validate an arbitrary user's deposit so they do not have guaranteed optionality over the execution of their deposit.
Resolution
Smardex Team: The issue was resolved in PR#677.
-
M-18 Medium No Rebalancer Trigger On Individual Liquidation Unexpected Behavior Acknowledged
Description
When closing a position the position has already been removed from the tick cumulatives as well as the exposure and balance of the aggregate long tracking.
When the close action is validated the vault balance is then incremented by the entire position value to account for this position being liquidated.
However since this liquidation logic is separated from the tick cumulative liquidation the rebalancer position will not be triggered when the individual position liquidation would be the one to set the net imbalance over the
_closeExpoImbalanceLimitBps.As a result the liquidation of a single large position upon the validation of it’s close action could adversely affect the balance of exposures without a counter-action from the rebalancer position.
Recommendation
Consider if this behavior and corresponding risk of imbalance without counter-action from the rebalancer position is acceptable.
If it is not, consider triggering the rebalancer after the individual position liquidation occurs in the
_validateClosePositionWithActionfunction.Resolution
Smardex Team: Acknowledged.
-
M-19 Medium Init Close Of A Liquidatable Pos Permitted Validation Resolved
Description
When a recent price is given to any init or validate function the
_applyPnlAndFundingAndLiquidateflow is executed and if too many ticks are liquidatable, some are left over and the flow breaks early as liquidations are pending.In this case, a user can initiate a close of a position that sits on such a liquidatable tick that was not liquidated yet by providing a price that is less recent than the current
lastPriceand therefore skipping the_applyPnlAndFundingAndLiquidateflow.As the init close pos function only checks if the version of the positions tick changed but not if the
lastPriceis above its liquidation price the call will pass.Recommendation
Consider not allowing any actions to be initiated or validated in the event that any liquidations are pending at the
lastPrice, even if thecurrentPricetimestamp is not recent.Resolution
Smardex Team: The issue was resolved in PR#722.
-
M-20 Medium Increased Liquidation Rewards Gaming Acknowledged
Description
Positions will be liquidated once the price drops below the current tick price. Liquidating multiple ticks with high expo, or with a big liquidation bonus (due to where the current price is at, compared to the tick liquidation price), will grant a higher liquidation reward. However, the total ETH reward calculated should not exceed 0.5 ETH.
Therefore, it can be more profitable for a user to liquidate ticks one by one, through the
initiateandvalidatefunctions (functions that will try to perform one iteration of liquidation if there are liquidatable ticks) ,instead of using theliquidatefunction that contains a capped liquidation reward.Recommendation
Cap the maximum rewards that can be gained from liquidations that are happening via initiate or validate functions if there are still pending liquidations.
These amounts should be capped such that the maximum reward that can be gained from individual liquidation should match with the max reward amount of multiple liquidations using
liquidate.It might be also good to consider limiting rewards that can be gained from liquidations in a single block. This can also incentivize the usage of
liquidate.Resolution
Smardex Team: Acknowledged.
-
M-21 Medium Extracting From Rebalancer Bonus Gaming Resolved
Description
Proof of concept: PoC
Rebalance mechanism is crucial for holding the protocol in balance, hence the users are incentivized to deposit funds to rebalancer (no fee for position opening, 80% of the remaining collateral from the liquidations will be distributed etc.)
However this mechanism can be gamed such that a user can sandwich a liquidation call to extract value from rebalancer bonus without a need for locking value in rebalancer more than a couple minutes.
Here are the steps to perform the attack:
1- Deposit into the rebalancer near liquidation and into the vault to increase imbalance. 2- Frontrun any user's liquidation attempt and perform the following in one transaction: 2.1- Validate rebalancer deposit 2.2- Liquidate (Which will create a rebalancer position) 2.3- Initiate a withdrawal from vault so that it will be possible to withdraw from rebalancer. 2.4- Initiate rebalancer close. 3- Wait 24 seconds (delay) and validate the withdrawal from vault and rebalancer close.
Result: Profit from bonus in rebalancer + pnl from trades in vault.
Recommendation
Prevent position closing from the rebalancer for some time after deposits so that there won't be a risk-free profit opportunity with the specified attack anymore.
Resolution
Smardex Team: The issue was resolved in PR#677.
-
M-22 Medium Depositors Banned From Rebalancer If Liquidated DoS Resolved
Description
Proof of concept: PoC
A rebalancer position is created when liquidations create an imbalance in the protocol. Although the rebalancer position liquidation price is low (it's configured to have max 3x leverage), there could be cases when this position might get liquidated.
If there are no new depositors or the imbalance is not big enough, this liquidation will not open a new rebalancer position. Users that were participating in the rebalancer position that got liquidated will now be temporarily banned from depositing into the rebalancer again.
This is due to the fact that the user's
entryPositionVersionis greater than the_lastLiquidatedVersion.Recommendation
If the rebalancer is liquidated, notify the rebalancer contract by executing
updatePositionbefore any early return.Resolution
Smardex Team: The issue was resolved in PR#642.
-
M-23 Medium Blocked Queue Due To Non-Validatable Action DoS Resolved
Description
Proof of concept: PoC
Users should be able to validate open positions as long as the price is above their liquidation price. In case of sudden price drops, user's validation price could be below their liquidation price, causing a revert on the leverage calculation.
As the validation price is a fixed Pyth update price during a certain time interval, it should always use the same exact update data. Once this blocked action becomes
actionable, any user creating a new action should also pass apreviousActionsDataparam, to validate the next actionable action in queue.Therefore, the blocked action will now DoS new protocol actions for 5 minutes, from
lowLatencyValidatorDeadlinetolowLatencyDelay. The admins will need to waitlowLatencyValidatorDeadline + 1 hoursin order to unblock this action and the user will lose the security deposit.Recommendation
Early return if the validation price is below the liquidation price, to signal a liquidatable state:
data_.isLiquidationPending = true;Resolution
Smardex Team: The issue was resolved in PR#639.
-
M-24 Medium Bad Debt Not Handled In Validate Withdraw DoS Resolved
Description
Validate Withdraw decreases the balance of the vault by the calculated
assetToTransferAfterFeesamt and does not cap it at zero. Therefore if this amt is bigger than the_balanceVault(for example as bad debt was taken between init and validate) an underflow occurs which leads to a long term DoS as this is a validation function.Recommendation
Handle bad debt in the validate withdraw function. For example, this can be done by capping
assetToTransferAfterFeestos._balanceVaultto avoid an underflow.Resolution
Smardex Team: The issue was resolved in PR#674.
-
M-25 Medium Closed Amt Not Seized If Its Value Is < 0 Validation Resolved
Description
The
_validateClosePositionWithActionfunction checks if the position is liquidatable and if so seizes it'scloseBoundedPositionValueand gives it the vault.This check happens with the neutral price and later on, it calculates the value of the position with the price rounded down by the pyth interval and reduced by the position fee.
Therefore the value of the position could be < 0, and in that case the
closeBoundedPositionValueis not seized and nothing happens as the rest of the function is only executedif (data.positionValue >0). In this case, thecloseBoundedPositionValueis stuck in the system.Recommendation
Seize the
closeBoundedPositionValueif the position value is <= 0.Resolution
Smardex Team: The issue was resolved in PR#679.
-
M-26 Medium User Can’t Close Rebalanced Position Validation Resolved
Description
The protocol has specific logic to ensure that any user can fully close their position if the position is apart of the rebalancer. The logic is intended to ignore the min long position amount if the user is coming from the rebalancer and the close amount is the full amount from that user.
However, the check fails to take into account any change in the users position value. So when the rebalancer is in profit the close amount will be greater than the position amount used in the check and visa versa for when the rebalancer is at a loss.
Because of this users will not be able to close their position if any pnl is experienced and there is only a small amount of assets in the rebalancer. Given that the rebalancer is a long position that can face liquidation having only a small amount in it is a real possibility.
Recommendation
When closing a position consider checking if the user is making a full close and if so, let the user bypass this check.
Resolution
-
M-27 Medium Liquidatable Positions Can Be Opened Validation Acknowledged
Description
In the validate open position flow if the maximum leverage is exceeded with the new
startPricethe position and it's tick is recalculated. This new tick could be liquidatable in edge cases if the price between init and validate changed drastically but the position is still created.The primary instance where this becomes a problem is when the liquidation tick actually increases when modified. In these situations a once healthy position can become liquidateable resulting in prior liquidation safeguards not being triggered and allowing liquidatable positions to be opened.
Recommendation
After modifying the positions leverage check if the position is liquidatable and if so, liquidate the position.
Resolution
Smardex Team: Acknowledged.
-
M-28 Medium Validate Deposit Ignores Funding Logical Error Resolved
Description
The
_validateDepositWithActionfunction uses either the available vault balance at init for the share price calculation or applies the PnL of the current price on it, whichever is bigger, but does not take funding into account.Therefore, if the longs paid funding fees to the vault between initiation and validation of a deposit the user receives more assets than the user should and the other way around.
Recommendation
Consider incorporating the latest vault funding into how many assets the users receive. Otherwise, if this is acceptable to the protocol, be sure to document the behavior so that users are aware.
Resolution
Smardex Team: Resolved.
-
M-29 Medium Sandwich Liquidations MEV Acknowledged
Description
When liquidations happen a stepwise jump in the value of shares happens. Either the liquidated value will increase the share value or the liquidation reward and/or bad debt will decrease it.
As the init call of deposit/withdraw saves the current balances and calculates the share/asset amt received based on that, users are able to sandwich liquidations to make profit or avoid losses. Deposit for profit:
- LP sees that a liquidation call will increase the vaults balance
- LP front runs the transaction and initiates a deposit to mint shares based on the old balance
- The liquidation call goes through
- The LP validates the deposit and is in instant profit without price changes
Withdraw to avoid loss:
- LP sees that a liquidation call will decrease the vaults balance (bad debt)
- LP front runs the transaction and initiates a withdraw to burn shares based on the old balance
- The liquidation call goes through
- The LP validates the withdraw and avoided paying for the bad debt and socialized more loss to the
other LPs by doing so
Recommendation
This finding serves only to document this behavior. Be aware of these potentially unexpected behaviors and how they could be manipulated.
Resolution
Smardex Team: Acknowledged.
-
M-30 Medium Validate Withdrawal Ignores Funding Logical Error Acknowledged
Description
The
_validateWithdrawalWithActionfunction uses either the available vault balance at init for the share price calculation or applies the PnL of the current price on it, whichever is less, but does not take funding into account.Therefore if the vault paid funding fees to the longs between initiation and validation of a withdrawal the user receives more assets than the user should.
In certain edge cases this can also lead to a underflow DoS of the
_validateWithdrawalWithActionfunction as this calculated withdrawal amount is later decreased from the_balanceVaultwhere funding was applied to.Recommendation
Consider incorporating the latest vault funding into how many assets the users receives. Otherwise if this is acceptable to the protocol, be sure to document the behavior so that users are aware.
Resolution
Smardex Team: Acknowledged.
-
M-31 Medium Remove Pending Position Price Can Be Stale Logical Error Acknowledged
Description
The
_removeBlockedPendingActionuses the_lastPriceto calculate the value of the stuck open long position. As this function does not update the price before performing this action it can potentially be stale.Recommendation
Fetch the current price at the beginning of the
_removeBlockedPendingActionfunction.Resolution
Smardex Team: Acknowledged.
-
M-32 Medium Neutral Price Used In Init Functions Validation Acknowledged
Description
All init functions use the
lastPrice(the latest neutral price) for calculations, while the validation functions use prices that were adjusted by the pyth interval up or down to round against the user.Therefore all the checks and temporary state updates at init are most likely wrong at validation time. Here are a few examples:
- Slippage checks
- Imbalance checks
- The temporary position between init and validate open position
- SDEX calculations
- When a stuck open position action is removed by the admin the user receives the position value
based on a unadjusted start price
These examples will lead to users entering a position at a price they explicitly did not agree too, Protocol reaching an imbalanced state and Incorrect amount of SDEX being burned.
Recommendation
Use the adjusted price in the init functions instead of the neutral price if the calculation uses the adjusted price in the validation function.
Resolution
Smardex Team: Acknowledged.
-
M-33 Medium Funding Rate Affected By Updates Logical Error Acknowledged
Description
In the
_fundingPerDayfunction the resulting_fundingPerDayvalue is a function of the current imbalance added to the existing EMA value. This resulting_fundingPerDayvalue is then factored into the EMA.The additive nature of the current skew to the current EMA for the resulting
_fundingPerDayvalue means that the more updates occur in a given timeframe the higher the funding will be.In the attached PoC shows that in a week period the funding is 34% greater if there is an update every day versus if there are only updates at the beginning and end of the period.
This is unexpected as the amount of updates should not affect the funding amount paid or the funding rate and instead this should be based purely on the skew experienced and the time of imbalance.
Recommendation
Consider refactoring the funding calculations such that the
fundingPerDayportion that is based upon the current imbalance as represented bynumerator^2 * _fundingSF / denominator^2is simply factored into the EMA as the latest data point instead of adding it to the EMA for the resulting_fundingPerDayvalue.The latest EMA computed this way including the most recent skew calculation can be used to compute the resulting funding value.
Resolution
Smardex Team: Acknowledged.
-
M-34 Medium Old Vault Validations Swing USDN Price Gaming Acknowledged
Description
Proof of concept: PoC
The behavior of the deposit flow is such that a user’s shares are not minted and their deposited amount is not added to the vault balance until validation time. This however causes several issues related to funding and the price of USDN. The minting of shares is an activity that changes the price of shares when the ratio used to mint is stale.
For example:
validationPriceis $80 from 30 minutes ago (initiation price is the same)- Current market price is $100
- Vault balance is 100 wstEth at the current market price
- Vault balance is 120 wstEth at the
validationPrice - Deposit is for 10 wstEth
- There are 10,000 total vault shares
- Divisor is 1
- USDN price is 100 wstEth * $100 / 10,000 shares = $1
- If the deposit validation were to occur at the current market price the user would receive 10 * 10,000 / 100 = 1,000 shares
- USDN price at the current market price would be: 110 wstEth * $100 / 11,000 shares = $1
- However the deposit validation takes place at the validation price, which is the most recent price after the initiation + 24 seconds.
- Therefore the user receives shares: 10 * 10,000 / 80 = 1,250 shares
- USDN price at the current market price is now: 110 wstEth * $100 / 11,250 shares = $0.9777
Thus the price of USDN experiences a stepwise change which can be non-trivial, a similar issue exists with withdrawals as well. Furthermore, there are other less obvious issues regarding funding with the current deposit accounting. Firstly, depositors are forced to be held accountable for the funding that occurs in the timeframe [
initiationTimestamp,validationTimestamp] even though their deposited amounts are not added to thevaultBalanceuntilvalidationTimestampand thus have not affected the skew for this period.Secondly, depositors are forced to be held accountable for the difference in funding which occurs over the [
initiationLastPriceTimestamp,initiationTimestamp] period versus the predicted amount of funding which would occur in that timeframe which is computed here:a Guardian proof of concept
ultLibrary.sol#L576.
Recommendation
Use the same approach as for the open position actions for the deposit actions. Mint the shares up front and let the deposited amount directly be added to the
vaultBalance, but do not give the shares to the user yet. Upon validation issue a correction to the shares received by the user based upon the price difference between initiation and validation.This will result in a stepwise jump in share price similar to the one experienced when using the
_validateOpenPositionUpdateBalancesfunction during the open position flow, however this stepwise jump will be far more insignificant than the one experienced currently due to old deposit validations.The same approach should be taken for vault withdrawals, similar to the closing of positions. Another solution would be to simply require all prices to be much more recent than the current configurations which would reduce the potential worse case magnitude of the stepwise jump described in this finding.
Resolution
Smardex Team: Acknowledged. 59
-
L-01 Low Fee Amounts Use Round Down Division Rounding Acknowledged
Description
Throughout the codebase rounding occurs in favor of the user when instead these operations should round in favor of the protocol. For instance, the fee charged to the user is rounded down in the
_prepareInitiateDepositDataand_validateDepositWithActionfunctions.Recommendation
Throughout the codebase and particularly in those areas mentioned, be sure to round in favor of the protocol instead of the user.
Resolution
Smardex Team: Acknowledged.
-
L-02 Low Incorrect Action Id Used Logical Error Resolved
Description
In the
_prepareClosePositionDatafunction when fetching the oracle price the actionId is calculated based on theparams.ownerinstead of the validator. This is invalid as the action id is intended to be based on the validator and the owner may not be the validator of the close position action.Recommendation
Use the
params.validatorfor the action id in the_prepareClosePositionDatafunction.Resolution
Smardex Team: The issue was resolved in PR#files#diff-fc903c7d38e9e68316fb4e9d65de2ea0994d47bcfe2a6716b01ce0d29018ab01L155.
-
L-03 Low Risk Of Immediately Liquidatable Positions DoS Acknowledged
Description
During action initiations if pyth data is not provided, the price used for the initiation can be up to the
_timeElapsedLimitold. This value is not configured in theinitializeStoragefunction, however in tests it is indicated that it may be on the order of an hour.This means it is likely that prices used on initiation may be inaccurate up to the tolerance of the on chain oracle. This should be carefully considered when configuring the
_safetyMarginBpsas there is an increased risk of immediately liquidatable positions when the lastPrice is stale up to the_timeElapsedLimit.Recommendation
The current configuration of 2% for the
_safetyMarginBpsis relatively safe as the deviation threshold for the ETH/USD Chainlink on-chain feed is 0.50%. However this risk should be carefully considered when configuring the_safetyMarginBpsor using another underlying asset.Resolution
Smardex Team: Acknowledged.
-
L-04 Low Rebalancer Depositors Might Not Receive Any Bonus Validation Acknowledged
Description
In the current implementation, the only additional incentive for rebalancer depositors is the potential bonus. This bonus is calculated as a percentage of
s._rebalancerBonusBps(set at 80%) from theremainingCollateralafter a liquidation:calculate the bonus now and update the cache to make sure removing it from the vault doesn't push the // imbalance above the threshold uint128 bonus; if (remainingCollateral > 0) {bonus = (uint256(remainingCollateral) * s._rebalancerBonusBps / Constants.BPS_DIVISOR).toUint128(); cache.vaultBalance -= bonus;}If the
remainingCollateralis below zero (resulting in bad debt), rebalancer depositors will not receive any bonus. Additionally, the bonus is split among all depositors.Therefore, if the total deposit amount in the rebalancer is large, the bonus received by each depositor will be negligible in comparison to their deposit.
Recommendation
To enhance depositor incentives, consider retaining a small percentage of each liquidation that does not result in bad debt and use this to increase a cumulative bonus state variable.
Once the rebalancer is triggered, the accumulated bonus from this state variable can be distributed to the depositors. After the bonus distribution, reset the bonus state variable to zero for the next cycle.
Resolution
Smardex Team: Acknowledged.
-
L-05 Low Lacking Configuration Validations Validation Resolved
Description
In the
setRewardsParametersfunction there are validations such that the gas usage values cannot be assigned above a set maximum. However there are no maximum bounds for thebaseFeeOffset,gasMultiplierBps,positionBonusMultiplierBps,fixedReward, andmaxRewardvalues.Recommendation
Consider if any of these values should be validated against a maximum and implement these validations as necessary.
Resolution
Smardex Team: The issue was resolved in PR#658.
-
L-06 Low Innacurate wstETH Price Calculation Unexpected Behavior Acknowledged
Description
The WstEthOracleMiddleware contract implements the function parseAndValidatePrice which fetches the
ethPricefrom Pyth/Chainlink and then calculates thewstETHprice by multiplying it by thewstEth.stEthPerToken. However, theethPricecould reflect a previous timestamp, while_wstEth.stEthPerTokenreflects the amount ofstETHperwstETHat the current block or timestamp. Typically, this discrepancy does not lead to significant issues becausewstEth.stEthPerTokenupdates only once per day, but if a rebase occurs between theethPrice.timestampand the currentblock.timestamp, thewstETHprice calculated would be inaccurate. In the Lido protocol,stETHrebases occur daily through reports from the Lido Accounting Oracle.The oracle submits data to the Ethereum network each day at approximately 12 PM UTC. These updates adjust
stETHbalances based on validator rewards, causing daily balance rebases. ThestETHtoken accrues rewards automatically, which are distributed across all holders in the form of daily balance adjustments. The rebase works by increasing thestEthPerTokenratio (the amount ofstETHthat represents 1 ETH) as staking rewards accumulate. ThestEthPerTokenratio is adjusted based on the total pooled ETH and the total number ofstETHtokens.The oracle calculates this based on data retrieved from the Ethereum consensus layer. If validators earn rewards, this ratio increases, reflecting the growth in staked ETH value. The formula looks like this:
stEthPerToken = totalPooledETher / totalstETHShares. In case of validator penalties or slashing events, the daily rebase could be negative, reducing the value ofstETHproportionally. This is managed through the same oracle mechanism, which reports any losses to the Ethereum network. The Lido protocol could enter in Bunker mode if significant penalties occur, in which case special handling rules apply to mitigate further losses and protect the network.In the last 2 submitted reports from the Lido Accounting Oracle:
stEthPerTokenwent from 1181096503086228134 to 1181205118477305019 increase of 0.0092%stEthPerTokenwent from 1181205118477305019 to 1181316084034843739 increase of 0.0094%
On the other hand, the Pyth price is always rounded against the user by using a percentage of its confidence interval (
_confRatioBps), which is initially set to the 40%. If we take a look at the latest Pyth data submitted on-chain for the ETH/USD price feed we get:- Price: 2380.87555000
- Confidence interval: 2.16445000
- Exponent: -8
- Publish time: 1728568800
Calculating the 40% of the confidence interval (2.16445000) results in 0.86578000. This confidence interval represents approximately the 0.0364% of the price, which is larger than the observed
stEthPerTokenincrease of around 0.01%. Based on our analysis, considering the currentstETHAPY and the small daily increases in thestEthPerToken, the risk of price manipulation, such as performing operations right before the daily rebase, is minimal. However, if staking rewards increase significantly or if rewards are distributed less frequently (e.g., monthly), leading to largerstEthPerTokenincreases, the risk of manipulation could become substantial and expose the system to potential price manipulation exploits.Recommendation
Consider using the
wstETH/USD Pyth Price Feed (0x6df640f3b8963d8f8358f791f352b8364513f6ab1cca5ed3f1f7b5448980e784) directly to accurately retrieve the price ofwstETHat any given timestamp.Resolution
Smardex Team: Acknowledged.
-
L-07 Low Stuck Action Functions Lack ReentrancyGuard Unexpected Behavior Resolved
Description
In the
UsdnProtocolFallbackcontract theremoveBlockedPendingActionfunctions do not have aninitializedAndNonReentrantmodifier. This allows potentially unexpected state updates to occur in the rare case where a trusted party re-enters into the system to call these functions during an action validation or initiation.Recommendation
Consider adding the
initializedAndNonReentrantmodifier to theremoveBlockedPendingActionfunctions.Resolution
Smardex Team: The issue was resolved in PR#694.
-
L-08 Low TickMath Rounding Documentation Acknowledged
Description
The
getTickAtPricefunction is subject to several rounding errors which cause prices which should correlate to one tick to correlate to a lower tick. This is because thelnWadfunction rounds down and this precision loss is not corrected for not accounted for in thegetPriceAtTickfunction. Thus there are cases where the result ofgetPriceAtTickdoes not agree with the result ofgetTickAtPrice.For example,
getTickAtPrice(getPriceAtTick(4)) == 3. This invalidates a core invariant of anyTickMathlibrary and leads to potentially unexpected cases. In thegetTickAtPricefunction when the ln value is positive, round down division is used. However in the case where 1 wei has been rounded off of the ln result this results in a full tick loss in precision. This is clear with the example of tick 4:LN_BASE= 99_995_000_333_308getPriceAtTick(4)= 1000400060004000098- lnWad(1000400060004000098) = 399980001333231
- 399980001333231 / 99_995_000_333_308 = 3.999999999999989999500008332883**…**
- 399980001333232 / 99_995_000_333_308 = 4
In fact in the positive ln case the ln value will only ever be 1 wei less than it ought to be when the imprecision occurs. Therefore it can be corrected by checking if imprecision has occurred and adding 1 wei if it has. The negative ln case experiences the same imprecision, except the
lnWadfunction rounds the magnitude down, not the signed value.However this cannot be resolved by simply deducting by 1 wei since the
LN_BASEvalue truncates precision. The truncation of theLN_BASEvalue ultimately yields imprecision which increases the result of the division by reducing the denominator. Because of this imprecision upwards on the magnitude of the division result, the roundUp division often exacerbates the precision loss. To resolve both of these imprecisions, round down division can be used for the negative case.With both of these adjustments in place, the
getTickAtPrice(getPriceAtTick(tick)) == tickinvariant holds. This rounding error rounds in the benefit of the protocol, treating the current price as if it were on a lower tick and thus performing liquidations earlier than they technically should occur. In this case the rounding error may be acceptable to the protocol and preferred over an alternative implementation.Recommendation
Be aware of this rounding imprecision, since it rounds in favor of the protocol the imprecision may be acceptable and this finding can serve to document the behavior.
Resolution
Smardex Team: Acknowledged.
-
L-09 Low _getLatestStoredPythPrice Lacks Validation Validation Resolved
Description
In the
_getFormattedPythPricefunction there is a validation that prevents the use of any pyth feed which reports a positive expo. This avoids a significant underflow issue which would occur in the_formatPythPricefunction where the expo is negated before being cast to a uint.However in the case where the low latency price is not used and the latest pyth stored price is queried with the
_getLatestStoredPythPricefunction the_formatPythPricefunction is called without first performing this validation on the pyth result.Recommendation
Consider adding the expo validation to the
_getLatestStoredPythPrice.Resolution
Smardex Team: The issue was resolved in PR#652.
-
L-10 Low Storage Compatibility Upgradeability Resolved
Description
The USDN codebase uses a UUPS upgradeability pattern, but not all abstract contracts use namespaced storage. This can create issues with future upgrades if the storage layout were to change as documented here: https://eips.ethereum.org/EIPS/eip-7201.
For example the
InitializableReentrancyGuardandUsdnProtocolStoragecontracts include storage variables which use the default assigned storage slot.Recommendation
Consider using the namespaced storage pattern for these storage values.
Resolution
Smardex Team: The issue was resolved in PR#666.
-
L-11 Low Unexpected Share Approval Reverts DoS Resolved
Description
The approved amount for the Usdn token is in tokens instead of shares. Therefore when attempting to transfer or burn shares from an address there may be unexpected reverts in the event of a rebase.
Consider the following scenario:
- User A is approved to spend 10 USDN tokens of User B
- Divisor is initially 1e18
- User A sends a transaction to the mempool to transfer 10 shares from User B with the
transferSharesFromfunction- A transaction triggering a rebase is recorded before User A's transaction and the divisor becomes
0.9e18
- User A's transaction is now attempting to transfer 10 / 0.9 = 11.11... tokens from User B, which
exceeds the allowance of 10 tokens
- As a result the transaction reverts
Recommendation
Clearly document this behavior so that integrators and users are aware of this risk.
Resolution
Smardex Team: The issue was resolved in PR#684.
-
L-12 Low Read Only Reentrancy Risk Reentrancy Resolved
Description
In the
_initiateDepositfunction thetransferCallbackis invoked before thes._pendingBalanceVaultis updated. This gives the caller control over the transaction execution while not all state updates have occurred for the deposit.This could potentially be used to exploit integrating systems which would rely on the imbalance checks which incorporate the
_pendingBalanceVault.Recommendation
Ensure all state updates have occurred for the deposit before making the external callback. This way untrusted actors can only take control of the transaction execution when the system is in a valid state.
Resolution
Smardex Team: The issue was resolved in PR#702.
-
L-13 Low Tick Penalty Changes Leverage Documentation Resolved
Description
The
desiredLiqPriceprovided by a user upon initiating the opening of a position corresponds to their liquidation tick price and not their effective liquidation price without penalty.The liquidation price without penalty is used to compute the position's exposure and thus leverage, therefore if the liquidation penalty changes unexpectedly before a user's transaction is recorded then the user's leverage could change significantly.
There is currently a user supplied
userMaxLeveragevalidation, however the position could have unexpectedly low leverage which would not trigger this validation.Recommendation
Consider documenting this risk to users. Otherwise consider introducing a
userMinLeveragevalidation.Resolution
Smardex Team: The issue was resolved in PR#710.
-
L-14 Low Missing Require Check In setMinLongPosition Validation Acknowledged
Description
The USDN protocol implements a setter function to update the
s._minLongPositionvalue:function setMinLongPosition (uint256 newMinLongPosition) external onlyRole(SET_PROTOCOL_PARAMS_ROLE) {s._minLongPosition = newMinLongPosition; emit IUsdnProtocolEvents.MinLongPositionUpdated(newMinLongPosition); IBaseRebalancer rebalancer = s._rebalancer; if (address(rebalancer) = address(0) && rebalancer.getMinAssetDeposit() < newMinLongPosition) {rebalancer.setMinAssetDeposit(newMinLongPosition);}}This value sets the minimum required amount of
wstETHfor a user to open a long position. However, the function lacks a validation check to ensure that thenewMinLongPositionparameter is greater than the currentmaxRewarddefined in theLiquidationRewardsManagercontract.If
s._minLongPositionis set to a very low value (e.g., 0.001wstETH), it could be exploited by a malicious user which could open multiple small positions, wait for them to become liquidatable, and then profit by liquidating them. The liquidation rewards would exceed the value of each individual position.Recommendation
Ensure that
maxRewardis never higher than the currents._minLongPosition.Resolution
Smardex Team: Acknowledged.
-
L-15 Low validateOpenPosition Might Not Be Completed Validation Resolved
Description
In the
validateOpenPositionfunction, the_prepareValidateOpenPositionDatainternal function is called in order to update the protocol balances, liquidate positions if the given price is recent and necessary and validate the open position action.If the Pyth price provided is not recent (older publish time than the current publish time of the protocol
lastPrice) liquidations will not be performed and the function will try to validate the open position action. This will occur often as the validation time given should belong toinitiateTimestamp+ 24. During the validation, the following check is performed:if (data_.lastPrice < data_.liqPriceWithoutPenalty) {the position must be liquidated data_.isLiquidationPending = true; return (data_, false);}If the protocol
lastPriceis lower thandata_.liqPriceWithoutPenaltythe_prepareValidateOpenPositionDatafunction will return the tuple(data_, false)wheredata_.isLiquidationPendingwill be set to true.Therefore the
_validateOpenPositionWithActionwould return the tuple(false, false)for(boolisValidated_, bool liquidated_)meaning that the position was not validated or liquidated. Basically the position was not validated because it was liquidatable but it was not liquidated because the Pyth price provided was not recent.At this point the action would remain in the queue and an extra transaction to liquidate the position would be required.
Recommendation
In this case, consider automatically executing the liquidation of the position using the current
lastPrice.Resolution
Smardex Team: The issue was resolved in PR#722.
-
L-16 Low USDN Depositors May Pay Less Fees On Withdrawal Unexpected Behavior Acknowledged
Description
Since the fees are distributed back to the vault on withdrawals a depositor can pay less fees by splitting their withdrawal up into two separate withdrawals, so that the second withdrawal gets a piece of the first withdrawal fee.
Recommendation
This behavior is likely not worth the complexity to fully address it. Simply be aware of this behavior.
Resolution
Smardex Team: Acknowledged.
-
L-17 Low Typo Typo Resolved
Description
In the
_applyPnlAndFundingfunction, it is mentioned that "in case of positive funding, the vault balance must be decremented ...". However this is incorrect as in the case of positive funding the long balance must be decremented.Recommendation
Correct the comment to state that, "in case of positive funding, the *long* balance must be decremented"
Resolution
Smardex Team: The issue was resolved in PR#645.
-
L-18 Low Insufficient Liquidation Incentives Incentives Acknowledged
Description
In the
_calcGasPricefunction the resulting gas price is capped to the lower of theblock.basefee +baseFeeOffsetand thetx.gasprice.However in the case where the
tx.gaspriceis higher than theblock.basefee + baseFeeOffset, then it is possible that the resulting liquidation rewards are an insufficient incentive to call the liquidate function.This can occur in the case where the
tx.gaspriceis significantly larger than theblock.basefee +baseFeeOffsetand when the tick being liquidated is at the current price tick, and thus no bonus reward is given.In this case it is likely that the
fixedRewardwould be exceeded in terms of gas costs for the liquidation call.Recommendation
Consider whether the insufficient incentive in this case is acceptable to the protocol. If it isn't, consider allowing the gas price used in the reward calculation to be the current
tx.gasprice.This should not be a large issue when the
tx.gaspriceis high as there is a configurablemaxRewardto limit the size of the liquidation reward to a reasonable price. Otherwise be sure to configure thebaseFeeOffsetwith this behavior in mind.Resolution
Smardex Team: Acknowledged.
-
L-19 Low Liquidations Untriggered By Recent Price Unexpected Behavior Acknowledged
Description
In the
_applyPnlAndFundingfunction the function early returns if the price is from the same timestamp as the currentlastPrice:if (timestamp <= lastUpdateTimestamp) {return Types.ApplyPnlAndFundingData({isPriceRecent: timestamp == lastUpdateTimestamp, tempLongBalance: s._balanceLong.toInt256(), tempVaultBalance: s._balanceVault.toInt256(), lastPrice: s._lastPrice});}The price is determined to be recent if
timestamp == lastUpdateTimestamp, and liquidations will be performed if the price is recent.However the liquidations will be performed with the
lastPriceinstead of thecurrentPricethus whenlastPrice != currentPriceliquidations may not occur when they should have.For example the existing
lastPricemay come from the last chainlink update at t=100 with a price of $97. Meanwhile the newcurrentPricemay be a Pyth update which also comes from t=100 and has a price of $96.50.The pyth update may more accurately represent the market price and there may be liquidations that need to occur at $96.50 but can not at $97.
Recommendation
Consider using the lower of the
lastPriceandcurrentPricein this scenario to trigger liquidations as conservatively as possible in the protocols favor.Resolution
Smardex Team: Acknowledged.
-
L-20 Low Inaccurate NatSpec Documentation Resolved
Description
In the documentation for the
_assetToRemovefunction in theUsdnProtocolActionsUtilsLibraryfile it is mentioned that theposExpoparameter represents the total exposure of the position. However this is often not the case as the_assetToRemovefunction is used for partial position decreases.Recommendation
Consider updating the documentation to specify that the
posExpoparameter represents the amount of exposure that is to be closed from the position rather than the entire exposure of the position.Resolution
Smardex Team: The issue was resolved in PR#618.
-
L-21 Low Rebalancer Always Utilizes All Available Liquidity Documentation Acknowledged
Description
The
_triggerRebalancefunction currently uses all the available liquidity in the rebalancer, along with the rebalancer's position value, to open a new long position. It calculates the rebalancer position’s tick through the_calcRebalancerPositionTickfunction, ensuring that the leverage of the opened position is at least equal to the protocol's minimum leverage. However, the minimum leverage,REBALANCER_MIN_LEVERAGE, is hardcoded to a value slightly above 1:uint256 internal constant REBALANCER_MIN_LEVERAGE = 10 ** LEVERAGE_DECIMALS + 1; // x1.000000000000000000001 Due to this extremely low minimum leverage, the following if code block is never entered, leading to scenarios where the rebalancer can open a new position with leverage as low as 1.000000000000000000001: check that the trading expo filled by the position would not be below the min leverage data.lowestUsableTradingExpo = positionAmount * Constants.REBALANCER_MIN_LEVERAGE / 10 ** Constants.LEVERAGE_DECIMALS - positionAmount; if (data.lowestUsableTradingExpo > tradingExpoToFill) {tradingExpoToFill = data.lowestUsableTradingExpo;}As a result, if the rebalancer has high liquidity, a new position with minimal leverage (just above 1) is created. In such case, rebalancer depositors will experience minimal profits because:
- The bonus is split among all rebalancer depositors, diluting the reward.
- The potential profit and loss (PnL) from the position is negligible, as the leverage is very close to 1, offering
little profit opportunity from price movements.
Moreover, after the rebalancer is triggered and all available liquidity is used to open a new position, the protocol may soon find itself in an imbalanced state, requiring another rebalance. This creates an additional challenge as the rebalancer can only be triggered by a liquidation event, which may be infrequent in certain scenarios.
Recommendation
Consider adjusting the minimum leverage threshold to a more meaningful value, ensuring that new rebalancer positions are opened with higher leverage, offering more substantial PnL opportunities for depositors. On the other hand, a major refactoring of the Rebalancer contract might be necessary.
Rather than using all available liquidity at once, the rebalancer should utilize liquidity in a more efficient, FIFO (First In, First Out) manner. This would prioritize users who deposited earlier, allowing their assets to be deployed first in the rebalancer. By doing so, the rebalancer could selectively draw liquidity as needed.
Resolution
Smardex Team: Acknowledged.
-
L-22 Low Rebalancer NonReentrant Modifiers Reentrancy Resolved
Description
In the Rebalancer contract, currently only the
initiateClosePositionfunction has anonReentrantmodifier. AnonReentrantmodifier will be added to theupdatePositionfunction to prevent a Critical issue which allows for the double closing of the Rebalancer position.However out of an abundance of caution it would be safest to add the
nonReentrantmodifier to the other user facing Rebalancer functions.Recommendation
Consider adding a
nonReentrantmodifier to all user facing functions in theRebalancercontract out of an abundance of caution.Resolution
Smardex Team: The issue was resolved in PR#725.
-
L-23 Low Missing Deadline Documentation Documentation Resolved
Description
In the documentation for the
_isActionablefunction in theUsdnProtocolVaultLibrarylibrary thelowLatencyDeadlineandonChainDeadlineparameters are undocumented in the functionNatSpec.Recommendation
Document the
lowLatencyDeadlineandonChainDeadlineparameters in theNatSpecfor the_isActionablefunction.Resolution
Smardex Team: The issue was resolved in PR#646.
-
L-24 Low Invalid Long Exposure Perturbs Accounting Documentation Acknowledged
Description
Proof of concept: PoC
During the initiation of an open position action, the position is effectively recorded as being opened from the protocol's perspective. The protocol accounts for this additional exposure on the liquidation tick and for the long aggregate accounting. However the trader is not held accountable for the PnL that occurs after initiation and before validation.
Therefore this exposure does not actually exist for the protocol, meanwhile the protocol thinks that it does. This is ultimately corrected in terms of the PnL calculations with the
_validateOpenPositionUpdateBalancesfunction, however before this correction is made the aggregate balances of the vault vs. longs is misattributed due to the "ghost" pnl of the initiated but not yet validated position.Firstly, this allows USDN shareholders to exit the vault before a trader's "ghost" losses have been corrected. Or enter the vault before a trader's "ghost" gains have been corrected. The risk of gaming through this vector is limited due to the two-step nature of vault deposits and withdrawals, however it gives an incentive for users to control the ordering of validations which occur to extract as much value out of the temporary mis-accounting.
Secondly, this perturbs the imbalance measuring since the system will count profit or losses from the temporary positions as if they have real exposure. Most notably, the rebalancer may be deemed unnecessary after a large liquidation, when in fact it should be used when accounting for the fact that "ghost" PnL should be corrected.
This behavior is however necessary to avoid giving trader’s a risk free opportunity for profit with pre-knowledge of their execution price and the inaccuracies should be small given that the earliest price in the range from initiation to validation is used.
Recommendation
This finding serves only to document this behavior. Be aware of these potentially unexpected behaviors and how they could be manipulated in unlikely scenarios.
Resolution
Smardex Team: Acknowledged.
-
L-25 Low Position Fee Not Refunded On Removal Unexpected Behavior Acknowledged
Description
In the
_removeBlockedPendingActionfunction when removing aValidateOpenPositionpending action the position fee which was charged to the user based on their entry price and given to the vault is not refunded to the user.Recommendation
Consider if this is the expected behavior. If it is not then consider refunding the amount of position fee paid to the
toaddress in the_removeBlockedPendingActionfunction.Resolution
Smardex Team: Acknowledged.
-
L-26 Low Gaming Vault Minted Shares Gaming Acknowledged
Description
Proof of concept: PoC
Users can deposit assets into the vault, using
initiateDepositand executing the deposit withvalidateDeposit. However, the initiate action does not enforce a specific oracle to be used.In the case of Chainlink, it will be allowed as long it’s not older than
_timeElapsedLimit, ifs._lastPricehas not been updated recently, and the published time is not older than the on chain Pyth price (which is not updated constantly in Ethereum).Users can then choose a specific old Chainlink update that will result in more USDN shares being minted, as long as the validation price is lower than the initiation price. In the following scenarios, we can observe that the
balanceVaultwill be increased, but the value will be lower than using a lower initiation price:initiate price > validate priceinitiatePrice: 1995 (Chainlink)balanceVaultat initiation: 98.67validatePrice: 1990.79balanceVaultadjusted: 98.88- USDC shares minted: 19,894.48
initiate price < validate priceinitiatePrice: 1979.9 (Chainlink)balanceVaultat initiation: 99.42validatePrice: 1990.79balanceVaultnot adjusted: 99.42- USDC shares minted: 19,786.67
Recommendation
Consider this scenario when assigning the max stale threshold for Chainlink using
_timeElapsedLimit.Resolution
Smardex Team: Acknowledged.
-
L-27 Low 0 Shares Wrapping Validation Resolved
Description
In the
_wrapSharesfunction the resultingwrappedAmount_may round down to zero when theusdnSharesamount is less than theSHARES_RATIO. This will result in a 0 amount of wrapped Usdn being minted to the user and a 0 amount of Usdn shares being transferred from the user.Recommendation
Consider validating that the
wrappedAmount_is greater than 0 in the_wrapSharesfunction to avoid unexpected wrap calls.Resolution
Smardex Team: The issue was resolved in PR#660.
-
L-28 Low Hardcoded Maximum Pyth Fee Validation Acknowledged
Description
As per the Pyth Network Documentation: *"The Pyth Network protocol has been designed to allow for the optional enablement of data fees in order to update the state of an on-chain price feeds. The ongoing existence of and size of the fee will be determined by governance on a per-blockchain basis; until governance is live, the fee will be 1 of the smallest denomination of the blockchain's native token (e.g., 1 wei on Ethereum)."*
The PythOracle contract implements this check:
uint256 pythFee = _pyth.getUpdateFee(pricesUpdateData); if (pythFee > 0.01 ether) {revert OracleMiddlewarePythFeeSafeguard(pythFee);}If the Pyth fee is set in the future to a value higher than 0.01 ether by the Pyth governance the USDN protocol would be totally blocked as all the interactions with the Pyth price feed would revert.
Recommendation
The
0.01 ethershould not be a hard-coded constant. Consider implementing a privileged function to update the max. Pyth fee supported.Resolution
Smardex Team: Acknowledged.
-
L-29 Low Rebalancer Tick Increment May Reduce Exposure Unexpected Behavior Acknowledged
Description
In the
_calcRebalancerPositionTickfunction when computing the rebalancer's liquidation tick there is a case to handle liquidation ticks that do not meet thelongImbalanceTargetBps. In this case a single tick spacing is added to the liquidation tick to increase the leverage and thus the exposure of the rebalancer.This is in hopes to fill the missing exposure so that the resulting imbalance will be within the
longImbalanceTargetBps. However the resultingliqPriceWithoutPenaltymay in fact be lower than the originalliqPriceWithoutPenaltyin the event that the liquidation penalty on the new liquidation tick is more than one tick spacing larger than the liquidation penalty on the old tick.In this case the resulting exposure after the adjustment by 1 tick spacing will be lower than before the adjustment. This further moves the long exposure away from reaching the
longImbalanceTargetBpsrather than towards it.Recommendation
This case will only appear in production if the liquidation penalty is assigned to a value that is greater than one full tick spacing above or below any liquidation penalty that was assigned in the past and is currently active on a tick.
No code change may be necessary if this fact is considered carefully while configuring updates to the liquidation penalty.
Otherwise if these updates would occur, it would be best to take the rebalancer position that corresponds to the highest exposure between the original determined rebalancer position and the position which has been adjusted up by one tick spacing.
Resolution
Smardex Team: Acknowledged.
-
L-30 Low Positive Value Liquidations May Still Cause Bad Debt Unexpected Behavior Acknowledged
Description
When a tick is liquidated, the
_sendRewardsToLiquidatorfunction is triggered. This function calculates the liquidation rewards using thegetLiquidationRewardsfunction, removes the rewards froms._balanceVaultand transfers them to the liquidator.However, the liquidation rewards are not closely tied to the remaining value of the liquidated tick. Even if a tick is liquidated late (with a negative remaining value, indicating bad debt), a fixed amount of wstETH is still sent to the caller and deducted from
s._balanceVault, which can increase/worsen the bad debt.Additionally, the closer the
currentPriceis to the liquidation price, the higher the position size bonus will be. This can result in a situation where, for instance, a tick with a positive remaining position value of only 1 is liquidated, but the rewards are high enough that a liquidation executed on time still leads to bad debt as these rewards are taken from the vault.Recommendation
Consider making liquidation rewards partially dependent on the remaining value of the liquidated tick. This would prevent over-rewarding liquidations that occur late or when the remaining value is low.
Resolution
Smardex Team: Acknowledged.
-
L-31 Low USDN Price Can Be Easily Manipulated Gaming Resolved
Description
Through the use of a “Proxy” contract it is possible to increase the vault and long exposure atomically. This “Proxy” contract can deploy other contracts that deposit and open long positions in a balanced way. We have performed multiple tests around this, and this is how the USDN price was affected.
In the tests below, the following approach was taken:
_usdnRebaseIntervalset to 1.- Deploy a “Proxy” contract.
- “Proxy” contract initiates multiple deposits/long position opens in a balanced way and using the same
lastPrice. - “Proxy” contract validates, 24 seconds later, all the initiated actions in the same order but with a price increase/decrease
(
percentage). For example, 85percentage, means that if the initiation price used was 1000, the validation price used was 850.- Final USDN Price represents the USDN price after all these validations were performed.
- The initial USDN Price in these tests was 1.00$.
Test #1:
- Initial Vault/Long exposure: Around 98e18.
- Total wstETH added through deposits/long positions: 100e18.
- Leverage of positions opened: 2x.
Test #2:
- Initial Vault/Long exposure: Around 98e18.
- Total wstETH added through deposits/long positions: 100e18.
- Leverage of positions opened: 4x.
Test #3:
- Initial Vault/Long exposure: Around 98e18.
- Total wstETH added through deposits/long positions: 500e18.
- Leverage of positions opened: 2x.
Test #4:
- Initial Vault/Long exposure: Around 98e18.
- Total wstETH added through deposits/long positions: 500e18.
- Leverage of positions opened: 4x.
In each of the tests it was observed that given a user with enough liquidity it is possible to manipulate the USDN price, possibly exposing it to speculation and opportunistic behavior. This "artificial" price alteration could create a favorable condition for anyone attempting to profit through short positions on USDN in external markets. In such an environment, a speculator could trigger a price movement, open a leveraged short position and benefit directly from the price drop that they themselves initiated.
Recommendation
_usdnRebaseIntervalshould be kept unset so that USDN rebases can occur constantly without delay, ensuring that the decrement of the divisor is as low as possible to prevent any potential manipulation. Another possible suggestion is restricting the amount of exposure that could be pending validation at the same time. This could also help mitigate the impact of the L-04 issue.Resolution
Smardex Team: The issue was resolved in PR#653.
-
L-32 Low Lacking securityDepositValue Validation Validation Resolved
Description
The
setSecurityDepositValuefunction performs no validation on the newsecurityDepositValue. Therefore thesecurityDepositValuemay be assigned to zero or an extraordinarily high amount.Recommendation
Consider introducing validations for the
setSecurityDepositValuefunction similarly to other setter functions to prevent invalid configurations.Resolution
Smardex Team: The issue was resolved in PR#658.
-
L-33 Low Depositors Pay Reduced Fees Logical Error Resolved
Description
During the validation of a deposit, the deposit fees are not added to the
balanceVaultstack variable before computing the amount ofmintedTokens, thus the depositor subsequently owns a portion of their own fees. This is clear if you imagine the scenario for a depositor with 10x the magnitude of the current vault balance:* fee percent = 1% * balanceVault = 10 * usdnTotalShares = 10 * deposit.amount = 100 * amountAfterFees = 99 * mintedTokens = amountAfterFees * balanceVault / usdnTotalShares * mintedTokens = 99 * 10 / 10 = 99 * usdnTotalShares = 109 * balanceVault = 110 * balance of bob's shares = 99 * 110 / 109 = 99.9 * Thus instead of paying a 1% fee, bob pays a 0.1% feeThough the normal circumstance will show a fee reduction much less than this.
Recommendation
Consider if this is the expected behavior or not. If it is not, consider adding the fee value to the
balanceVaultbefore computing themintedTokensamount so that depositors pay the full fee amount to the other vault depositors.If this approach is taken then care should be taken to update the
usdnSharesToMintEstimatedvalue in the_prepareInitiateDepositDatafunction.Resolution
Smardex Team: The issue was resolved in PR#635. 92
-
L-34 Low Potential Precision Loss In _positionValue Calculation Unexpected Behavior Acknowledged
Description
In the current implementation, the
_positionValuefunction calculates the value of a position based on the current price, liquidation price without penalty and the position’s total exposure.However, due to Solidity’s integer division, there is a risk of significant precision loss when the
currentPriceis very close toliqPriceWithoutPenaltyor when the closed exposure is very small relative to thecurrentPrice:function _positionValue(uint128 currentPrice, uint128 liqPriceWithoutPenalty, uint128 positionTotalExpo) internal pure returns (int256 value_) {if (currentPrice < liqPriceWithoutPenalty) {value_ = -FixedPointMathLib .fullMulDiv(positionTotalExpo, liqPriceWithoutPenalty - currentPrice, currentPrice) .toInt256();} else {value_ = FixedPointMathLib.fullMulDiv(positionTotalExpo, currentPrice - liqPriceWithoutPenalty, currentPrice).toInt256();}}When the
currentPriceis very close to theliqPriceWithoutPenalty, or when the exposure is extremely small, the result of the division can be rounded down. This can lead to the user not receiving the correct payout, especially during a long position closure, causing a loss of funds.Recommendation
Consider enforcing a minimum
amountToClosein theinitiateClosePositionfunction to prevent situations where the assets received are either zero or significantly reduced due to rounding.This would ensure that users avoid losses caused by closing very small positions where precision loss or rounding can result in negligible payouts.
Resolution
Smardex Team: Acknowledged.
-
L-35 Low TotalSupply Exceeded By User Balances Rounding Resolved
Description
The
totalSupplyof the Usdn token uses the_convertToTokensfunction withRounding.Closest, however due to precision loss, this may disagree with the summation of each individual user's balance.For example, consider the following scenario:
_totalSharesare 10e36 + 10 wei- divisor is 1e18
totalSupplyis computed as 10e36 + 10 wei/1e18 = 10e18 with closest rounding- User A holds 1000000000000000000500000000000000010 shares
- User A balance is 1e36 + 0.5e18 + 10 wei / 1e18 = 1e18 + 1 wei (1000000000000000001)
- User B holds 8999999999999999999500000000000000000 shares
- User B balance is 9000000000000000000
- The summation of User A and User B Balances is 10e18 + 1, but the
totalSupplyis reported as
10e18.
There is no immediate large impact for the USDN system, however this is worth noting especially for consumers of the USDN token balances amounts.
Recommendation
Be sure to document this inconsistency for integrators and consumers of the USDN balances.
Resolution
Smardex Team: The issue was resolved in PR#728.
-
L-36 Low getHighestPopulatedTick Might Return An Incorrect Tick Unexpected Behavior Resolved
Description
The function
getHighestPopulatedTickis used to retrieve the highest tick with an open position:function getHighestPopulatedTick() external view returns (int24) {return s._highestPopulatedTick;}However, when the last position of a tick is fully closed the
_removeAmountFromPositionfunction does not update thes._highestPopulatedTickwith the new highest populated tick.Therefore, in cases where the last position in the highest populated tick is closed, the tick remains marked as the highest populated despite having no open positions.
This issue does not impact liquidations or cause any significant effects, aside from temporarily displaying an incorrect value for
s._highestPopulatedTick. The incorrect tick will persist until either a liquidation occurs or a new position is opened at a higher tick.Recommendation
Consider updating the
s._highestPopulatedTickwhen the last position of a tick is closed.Resolution
Smardex Team: The issue was resolved in PR#705.
-
L-37 Low No Slippage On Validation Validation Acknowledged
Description
When validating the opening of a position the real entry price of a position is determined. The actual entry price experienced by a position on validation cannot be predicated as it is subject to price changes between the initiation and validation time frame.
A user is currently able to specify a
maxPricewhich the position can be opened at, however this is only enforced on initiation unlike themaxLeverage. Thus a trader can have their position opened past theirmaxPricewhich may be unexpected.However currently this is necessary to avoid risk free trade opportunities in the timeframe between the validation price and the validation timestamp.
Recommendation
Be sure to document this behavior to users so that they are aware of this when assigning the
maxPricevalue.Resolution
Smardex Team: Acknowledged.
-
L-38 Low _triggerRebalance Does Not Account For The Current Action Effect Unexpected Behavior Acknowledged
Description
The
_triggerRebalancefunction is responsible for initiating a rebalance when the protocol experiences a significant imbalance on the long side. It calculates the rebalancer's new position tick using the_calcRebalancerPositionTickfunction, aiming to bring the protocol back to a balanced state.However, this function does not account for the effects of ongoing actions, such as an initiation or validation, which may impact the current state of the protocol. When an action like position initiation or validation is in progress, the actual state of the protocol will change, but
_triggerRebalanceoperates based on the previous state, failing to consider these changes.This could result in inaccurate rebalancing, as the rebalancer may open a position that does not reflect the real-time protocol conditions.
Recommendation
Even though this could require some major refactoring, consider executing the initiation or validation actions before calling
_triggerRebalance. By doing so, the rebalancer can account for any changes these actions introduce, ensuring that the rebalancing is based on the updated protocol state.This approach would prevent the rebalancer from operating on outdated information and help maintain a more accurate balance in the protocol.
Resolution
Smardex Team: Acknowledged.
-
L-39 Low DOS State Due To s._minLongPosition Restriction Validation Acknowledged
Description
The protocol implements several safeguards to maintain balance between the long side and the vault side:
_checkImbalanceLimitDeposit: Ensures that the protocol does not allow deposits that would create an
imbalance beyond the vault side's deposit limit.
_checkImbalanceLimitWithdrawal: Ensures that withdrawals do not cause an imbalance exceeding the long
side's withdrawal limit.
_checkImbalanceLimitOpen: Prevents the opening of new long positions that would cause an imbalance
beyond the open limit on the long side.
_checkImbalanceLimitClose: Prevents closing positions if it would create an imbalance beyond the close
limit on the vault side.
Additionally, the protocol enforces the
s._minLongPositionvariable, which sets the minimum allowable size for long positions. As users interact with the protocol, a highly balanced state could emerge. However, in such a scenario, users might face difficulties in opening new positions with the minimum leverage allowed bys._minLongPosition.This would occur because the position size would not satisfy the
_checkImbalanceLimitOpenrestriction. Furthermore, users may also be unable to close their open positions as:- Attempting to fully close a position would fail because it would violate the
_checkImbalanceLimitWithdrawal.- Attempting to partially close a position would revert because the remaining position would fall below the
s._minLongPosition.This situation would block users on the long side, leaving them exposed to liquidation risks. To resolve this issue, a privileged account would need to invoke the
setMinLongPositionfunction to temporarily lower thes._minLongPositionthreshold or adjust theopenExpoImbalanceLimitBpsandcloseExpoImbalanceLimitBpsto expand the imbalance limits.Recommendation
Ensure that the exposure on both sides (vault and long) is sufficient to accommodate the opening of new positions with the minimum allowed position size (
s._minLongPosition) and the lowest leverage. This is particularly important during the initialization phase of the protocol, when theinitialize()function is called.Resolution
Smardex Team: Acknowledged.
-
L-40 Low _usdnRebaseInterval Unassigned On Initialization Unexpected Behavior Resolved
Description
In the
initializeStoragefunction the_usdnRebaseIntervalvalue is not assigned with an initial value. This may be intended if the initial value for_usdnRebaseIntervalis intended to be zero. However if the initial value should not be zero then the initialization is missing.Recommendation
If the initial
_usdnRebaseIntervalvalue should not be zero then initialize it to the appropriate value.Resolution
Smardex Team: The issue was resolved in PR#653.
-
L-41 Low Full Operational Halt During Protocol Pause Unexpected Behavior Acknowledged
Description
When the protocol enters a paused state all operations are suspended. This includes:
- Initiating any type of new action.
- Validating any ongoing actions.
- Performing liquidations.
As a result, users are unable to respond to market price fluctuations since they cannot close their positions.
Furthermore, because liquidations cannot be executed while the protocol is paused, it is highly likely that upon unpausing the protocol, there will be multiple liquidations involving bad debt, as once the protocol resumes, positions that should have been liquidated earlier could be severely underwater.
Recommendation
It is recommended to either:
- Allow liquidations during the paused state: By enabling liquidations while other operations remain
paused, the system could still manage risk and prevent bad debt from accumulating. 2. Implement a granular pausing mechanism: Rather than pausing the entire protocol, consider splitting the pause functionality to selectively suspend non-critical functions. For example, initiation of new actions and validations could be paused, but liquidations and certain other risk management functions could continue to operate, ensuring the system remains secure while allowing for flexibility.
Resolution
Smardex Team: Acknowledged.
-
L-42 Low Update Events Emitted On No Update Events Acknowledged
Description
In the Fallback contract there are many setter functions which emit events for the assignment of new values for important addresses and configurations.
However in these setter functions there may be no update to the stored values when the new value is the same as the existing value, meanwhile the update event is still emitted. This may cause confusion for consumers of these events as no update has actually occurred.
Recommendation
Consider validating that the new value is not the same as the old value in the setter functions to avoid emitting update events when no update has occurred.
Resolution
Smardex Team: Acknowledged.
-
L-43 Low Deposits Experience Invalid Funding Logical Error Acknowledged
Description
Vault deposits do not affect the skew of USDN vault exposure vs. trader exposure because the
_pendingBalanceVaultvalue is not included in the funding calculation.However deposits are still affected from the funding which occurs in the timeframe from [initiate., validate] since the original
balanceVaultis used to compute the assets they receive. For example:- Vault balance upon initiate deposit: 10
- Funding occurs which adds 5 to the vault bal
- Vault bal used in validate deposit is the cached 10
- Shares = assets * shares /
totalAssets= 10 * 10 / 10 = 10 shares
Current value of the 10 shares = 10 * 25 / 20 = 12.5, the depositor got the funding from [initiate, validate] but the depositor didn’t affect the skew.
As a result a large deposit could siphon funding from the depositors who rightfully should have received it. Additionally, if this large deposit was otherwise included in the
vaultBalancethen the vault would have been paid less funding or may have even had to pay funding.Recommendation
Consider using a vault balance which is affected by the funding between initiate and validate to compute the shares received by the depositor.
Resolution
Smardex Team: Acknowledged.
-
L-44 Low Insufficient removeBlockedPendingAction Wait Validation Resolved
Description
The
_removeBlockedPendingActionfunction may not be invoked withins._lowLatencyValidatorDeadline + 1 hoursof the initiation of an action. This is to give users ample time to validate the action before it is removed from the queue.The current
_onChainValidatorDeadlineis set to 65 minutes, meaning that once the low latency delay has passed arbitrary users will not have the chance to validate if it is removed using the_removeBlockedPendingActionfunction at thes._lowLatencyValidatorDeadline + 1 hourstime.Recommendation
Consider if this is acceptable to the protocol. If it is not, consider configuring the
_removeBlockedPendingActionfunction delay to be a function of the low latency delay plus some wait time to allow arbitrary users to validate the action with an on chain oracle before it can be removed.Resolution
Smardex Team: The issue was resolved in PR#719.
-
L-45 Low LastUpdateTimestamp Errantly Initialized Logical Error Acknowledged
Description
In the
initializefunction the_lastUpdateTimestampvalue in storage is assigned to theblock.timestamp, however in all other assignments the_lastUpdateTimestampis assigned with the timestamp corresponding to the provided price.As a result there may be unexpected edge cases directly after initialization where subsequent actions use a price which is more recent than the one used on initialization but the
lastPriceis not updated because it has been assigned to the timestamp of initialization which is more recent.For example:
- Initialization occurs at t = 100
- The initialization uses a price from t = 50
- A subsequent open order is initiated with a price from t = 60
- The open order is validated with a price from t = 90
In this example the price used to validate the open order is technically the latest price, but will not be treated as such. This can cause unexpected behavior for the PnL correction which occurs during the validation of an open order. As well as prevent liquidations from occurring as the price is not deemed recent.
Recommendation
Consider initializing the
_lastUpdateTimestampto the timestamp of the price being used upon initialization.Resolution
Smardex Team: Acknowledged.
-
L-46 Low Potential Reverts From New Actionable Actions DoS Resolved
Description
Whenever a user initiates or validates an action, they must also validate the next actionable action in the queue, if there is one. A corresponding
previousActionDatamust be sent as a parameter.However, there may be no actionable actions when the transaction is submitted, but an action can become actionable when the transaction is executed. If the user sends empty
previousActionData, the entire transaction will revert.Recommendation
Consider allowing empty previous action data to be empty and avoid triggering
_executePendingActionOrRevertin this case.This way users can avoid potentially unexpected reverts when the queue is empty and may become populated depending on the block in which the transaction is recorded.
Resolution
Smardex Team: The issue was resolved in PR#701.
-
L-47 Low Incorrect Event Emitted For Close Actions Events Acknowledged
Description
There is an edge case where the validation price of open position is below the liquidation price, but the tick was never liquidated. In case of partial closing positions, the protocol will incorrectly emit
LiquidatedPositionevent, as the user can still create a new close position action.Recommendation
Only emit the event if the validate action is a complete close position amount. Otherwise consider indicating that this was a partial position liquidation.
Resolution
Smardex Team: Acknowledged.
-
L-48 Low Burned SDex Unforgiven On Removal Unexpected Behavior Acknowledged
Description
In the
_removeBlockedPendingActionfunction the deposited asset amount is returned to thetoaddress, however the burned sDex amount is not.Recommendation
Be aware of this loss of burned sDex for the depositor and consider making them whole if this situation with the
_removeBlockedPendingActionfunction arises.If this should be resolved manually on a case-by-case basis, then consider storing the amount of sDex burned on the deposit action and minting this amount back to the
toaddress in the_removeBlockedPendingActionifcleanupistrue.Resolution
Smardex Team: Acknowledged.
-
L-49 Low Imbalance Variables Updates Validation Resolved
Description
Variables related to imbalance are crucial to protect delta-neutrality of USDN and also prevent different attack vectors. Hence it is utmost important to be careful when changing these.
While function
setExpoImbalanceLimitshave some checks in order to limit these values, there are still some values that are possible but shouldn't be possible.For example, the
RebalancerCloseExpoImbalanceLimitBpsshould never be bigger thanlongImbalanceTargetBps. But this is not checked in the setter function. If this ever happens, then it would be possible to leave the rebalancer position immediately after rebalancing happened.Recommendation
Consider adding validation for this specific case and general upper and lower limits as necessary to avoid any risk of potential misconfiguration.
Resolution
Smardex Team: The issue was resolved in PR#703.
-
L-50 Low Preview Functions Not Accurate Unexpected Behavior Resolved
Description
Both
previewDepositandpreviewWithdrawuse the exact price passed for calculations, but confidence interval is not applied. Additionally, if the real validate action triggers a rebase, the tokens minted will be completely different.Therefore, these preview functions will not accurately calculate real amounts. If using these functions to determine the
sharesOutMinoramountOutMin, the transaction might revert.Recommendation
If this is intended behavior, document it to the users, so they are aware that these functions are only for rough estimates.
Resolution
Smardex Team: The issue was resolved in PR#700.
-
L-51 Low Liquidations May Occur Earlier Than Expected Logical Error Acknowledged
Description
Users might expect they will be liquidatable only when the price is exactly reaches to the liquidation price. But because of rounding down of prices, positions can be liquidated earlier than expected.
Recommendation
Inform users about this behavior and if possible, share the exact price at which the liquidation will occur.
Resolution
Smardex Team: Acknowledged.
-
L-52 Low Protocol Avoids Correct TVL Growth Warning Acknowledged
Description
According to current initialization parameters, the protocol will start in a balanced state and with a 5% maximum imbalance limit which if reached will prevent any actions until the imbalance is corrected.
With the current initialization parameters, a position with 2 wstETH and around 3.7x leverage will be enough to put the protocol to the limit which after that traders will need to wait for new vault deposits to be able to open new long positions.
This process will continue for a long time which will create a stair-stepping case to scale TVL and will slow down the scaling phase.
Recommendation
Consider these parameters for initial setup with this in mind, especially the maximum imbalance limits which can be bigger initially to make scaling faster.
Resolution
Smardex Team: Acknowledged.
-
L-53 Low Imbalance Breached While Validating Open Logical Error Resolved
Description
The leverage of a position is recalculated in validation according to the new price. Which will change the exposure of the position and will change the imbalance amount in the protocol.
While maximum imbalance checks are performed during initiate actions, it is not checked during validation actions. This can naturally lead to imbalance breaches.
Recommendation
Be aware of this risk and document it for integrators and users.
Resolution
Smardex Team: The issue was resolved in PR#712.
-
L-54 Low Validators With No Receive Function Documentation Resolved
Description
If a user uses an address that can't receive ether as a validator, the validator won't be able to validate the action.
This itself is not a problem for the protocol considering these actions will become actionable by anyone after some time. But it will lead to loss of funds for the user who is unaware of this fact.
Recommendation
Inform users about this behavior properly and let them be sure that the address used for validator will be able to receive ether.
Resolution
Smardex Team: The issue was resolved in PR#704.
-
L-55 Low USDN May Temporarily Lose Peg Warning Resolved
Description
The variable
_usdnRebaseIntervalis not used in initial deployment. If this variable non-zero, then it will be possible for USDN price to increase unexpectedly temporarily.This can happen because although there is a liquidation, aforementioned variable can prevent rebases which will inflate the value of USDN.
Users will be able to arbitrage the USDN price during this time, as the next rebase will push price down to target again.
Recommendation
Consider always triggering the rebase if the price is recent.
Resolution
Smardex Team: The issue was resolved in PR#653.
-
L-56 Low Funding Retroactively Affected By Admin Update Logical Error Acknowledged
Description
The
SET_PROTOCOL_PARAMS_ROLEis allowed to make critical storage variable changes using admin functions. Specifically,setFundingSF,setEMAPeriod, andsetProtocolFeeBpsinvolve updates that will impact the outcome of PnL and Funding calculations.Therefore, failing to execute
applyPnlAndFundingbefore the admin update will cause the following values to accrue based on the new state variables, when they should instead accrue based on the old values until the changes occur:_fundingPerDayis calculated based ons._fundingSF_calculateFeeusess._protocolFeeBpsto calculate protocol fee on funding value._updateEMArelies ons._EMAPeriodfor the EMA calculation.
Recommendation
Before making critical state changes, consider triggering an update using
UsdnProtocolCoreLibrary._applyPnlAndFunding.Resolution
Smardex Team: Acknowledged.
-
L-57 Low Sparse Queue After Pyth Validation Ends Unexpected Behavior Partially resolved
Description
The
getActionablePendingActionsreturns the actionable pending actions, up to a max of 20. In case we have a pending action that needs Chainlink validation, this action will stay in the front of the queue for some decent amount of time.Therefore, the pending action can make the queue grow, as long as the first and last action in the queue are not actionable. There could be cases where a queue of 20+ actions are pending (even if most actions in the middle are empty or cleared).
If an action in the queue becomes actionable, and the position in the queue is at an index greater than 20, the
getActionablePendingActionswill not catch this action and may even return an empty array.Additionally, the internal
_executePendingActionwill also fail to execute any actionable action, as the max amount of items read from the queue is 20 (MAX_ACTIONABLE_PENDING_ACTIONS).Recommendation
Consider increasing the max actionable pending actions value, or refactoring the function so it can ignore empty actions in the queue so it’s easier to find the actionable actions.
Resolution
Smardex Team: The issue was resolved in PR#701.
-
L-58 Low Instantaneous Actionable Actions Unexpected Behavior Acknowledged
Description
Validator deadline timestamps can be updated using
setValidatorDeadlinesadmin function. The new value for_lowLatencyValidatorDeadlinestate variable can range between 60 seconds and thelowLatencyDelay(20 minutes).Reducing this deadline may cause some pending actions to become actionable instantly. Any user can then validate the pending action and claim the security deposit.
Recommendation
Document this behavior to the users, making sure they are aware of deadline updates and potentially lose their security deposits if they don't validate their actions on time.
Resolution
Smardex Team: Acknowledged.
-
L-59 Low Missing Validation For Admin Functions Validation Resolved
Description
Variables that can change with setter functions are not validated properly. Using a value that would create problems for these variables can be catastrophic and the effect of it can be instant.
Hence please consider following checks in setter functions:
1-
setOracleMiddleware: check if thelowLatencyDelayin the new middleware is within bounds compared to deadlines. 2-setRebalancer: CompareminLongPositionto minDepositAssets in new contract. 3-setValidatorDeadlines: These should give more room to [LowlatencyValidatorDeadline,lowLatencyDelay], as it can be 1 second apart and lead to problems. 4-setMaxLeverage: Having a max cap of 100x is too much to be handled in Ethereum, which leads to increased risk for bad debt.Recommendation
Put further validations in the setter functions mentioned above.
Resolution
Smardex Team: The issue was resolved in PR#716.
-
L-60 Low Incorrect Return Value For Failed Actions Composability Resolved
Description
Initiate actions have a return value
successthat should determine if the action was initiated or not. However,initiateClosewill return true if the position is liquidated, which is unexpected. External actors may have issues integrating USDN protocol, creating unexpected behaviors.Recommendation
Consider returning false if the action was not initiated.
Resolution
Smardex Team: The issue was resolved in PR#692.
-
L-61 Low Leverage DoS With lastPrice Update DoS Acknowledged
Description
Leverage for the position will be calculated according to the
lastPricevariable in the protocol. When a user provides a non recent price from block timestamp t, if there is a price stored in the protocol after timestamp t, leverage will be calculated according to that specificlastPrice.Hence the user who provides a desired liquidation price with their submitted price in mind, can see that their leverage changed because the price used for the position will be lastPrice.
However, if the user tries to open a position with maximum leverage, it is possible that this action will revert when using
lastPriceas leverage will exceed maximum leverage limit.Recommendation
Document this behavior to the users so that they can act accordingly and prevent themselves from this small DoS edge case.
Resolution
Smardex Team: Acknowledged.
-
L-62 Low Small Rebalancer Positions Are Allowed Validation Acknowledged
Description
In the
_triggerRebalancerfunction of theUsdnProtocolLongLibrary, the current implementation allows for extremely small positions:This check permits positions larger than 1/10000th of the
_minLongPosition. Such small positions can lead to situations where the position's collateral is less than the rewards a user could receive for liquidating it. This discrepancy creates a potential for bad debt that the vault would have to absorb.The issue arises from the following factors:
- The liquidation reward are designed to be profitable by getting larger as gas costs increase.
- The liquidation logic does have a max reward cap, but this is based on the typical min collateral
amount for non-rebalancing users which is much larger than what the rebalancer requires.
Due to these factors small positions made by the rebalancer can lead to small amounts of bad debt accrued over time.
Recommendation
Be aware of the risk of a small amount of bad debt in these rare cases. If this is not acceptable then consider introducing a case where if the remaining collateral is greater than 1/10000th of the
_minLongPositionbut less than the_minLongPositionthen the funds are given back to the Rebalancer instead of seeded into a new position or given to the vault.Resolution
Smardex Team: Acknowledged.
-
L-63 Low Some ERC20 Tokens Are Incompatible Documentation Resolved
Description
Throughout the codebase
_assetamounts are pushed to arbitrary addresses in validation actions.For wstEth there is no way for these transfers to revert unless they run out of gas, however for tokens which have a blacklist, callback or other unique behaviors when transferring may revert and cause the queue to enter a stuck state.
Recommendation
Be aware that the USDN system is incompatible with assets that have a blacklist, callback or other unique functionality.
Resolution
Smardex Team: The issue was resolved in PR#648.
-
L-64 Low Liquidatable Positions Can Be Transfered Documentation Acknowledged
Description
When the
transferPositionOwnershipfunction is called the protocol does not allow a transfer of a position which tick was liquidated.But it does not check if the position is healthy before by calling
_applyPnlAndFundingAndLiquidate. Because of this, it is possible for liquidatable positions to be transferred.Recommendation
If this is not the expected behavior, consider checking if the position is liquidatable by calling
_applyPnlAndFundingAndLiquidatebefore performing the checks intransferPositionOwnership. Otherwise this finding serves to document this behavior.Resolution
Smardex Team: Acknowledged.
-
L-65 Low Sandwich Validate Close Pos MEV Acknowledged
Description
When close position action is validated a stepwise jump in the value of shares happens. The vault balance can increase or decrease based on the price changes between init and validate or if the price falls below the positions liquidation price the full value could be seized by the vault.
As the init call of deposit/withdraw saves the current balances and calculates the share/asset amount received based on that, users are able to sandwich validate close position calls to make profit or avoid losses.
Deposit for profit:
- LP sees that a validate close pos call will increase the vaults balance
- LP front runs the transaction and initiates a deposit to mint shares based on the old balance
- The validate close pos goes through
- The LP validates the deposit and is in instant profit without price changes
Withdraw to avoid loss:
- LP sees that a validate close pos will decrease the vaults balance
- LP front runs the transaction and initiates a withdraw to burn shares based on the old balance
- The validate close pos goes through
- The LP validates the withdraw and avoided paying for the loss and socialized more loss to the other
LPs by doing so
Recommendation
This finding serves only to document this behavior. Be aware of these potentially unexpected behaviors and how they could be manipulated.
Resolution
Smardex Team: Acknowledged.
-
L-66 Low Rebalancer Only Triggered On Liquidations Logical Error Acknowledged
Description
The rebalancer is only potentially trigger if liquidations happened. But because of funding it could be that there is a large enough imbalance to reach the threshold even without liquidations.
Recommendation
Consider rebalancing the pool even if there are no liquidations, when the imbalance is large enough.
Resolution
Smardex Team: Acknowledged.
-
L-67 Low Rebalancing Position Impacts Traders Documentation Acknowledged
Description
When rebalances occur, existing traders end up closing with less funds than if there was no rebalance at all. This will result in an inconsistent amount of funds the trader can withdraw.
This is because of truncation that can occur when the trader's leverage is low enough that the long balance exceeds 99% of the exposure. In cases like this, all traders will have some of their long balance truncated and sent to the vault.
This is possible due to the unbounded amount of assets that a user can deposit into the rebalancer, but for truncation to occur, an immense amount of capital is required.
Recommendation
Because the inconsistencies are less than $0.01 in every case besides truncation and for truncation to occur, it would require tens of millions of dollars to be deposited into the rebalancer at once, it is recommended that this be documented and that traders closing amounts and rebalancing activities monitored.
Resolution
Smardex Team: Acknowledged.
-
L-68 Low Sandwich Deposit/Withdraw MEV Acknowledged
Description
When users initiates a deposit or withdrawal the current state of the system is saved in the pending action and the user mints shares on validation based on the state at init.
This can be abused by sandwiching a deposit or withdraw validation call if the share price changed significantly between init and validation of the action.
For example:
- A user initiates a deposit of 100 assets at a share price of 1 asset == 1 share
- The user validates the deposit after a while
- An attacker sees that the share price changed to 0.9 assets == 1 share (funding, liquidations with
bad debt, ...) and front runs the validation call to initiate a deposit
- The first user's validation call goes through and the user receives 100 shares for depositing 100
assets and these shares are now worth 90 assets
- The validation call of the attacker goes through and the attacker gains more value then if they did
not front-run at all as the other user's deposit is socialized among all LPs.
Recommendation
This finding serves only to document this behavior. Be aware of these potentially unexpected behaviors and how they could be manipulated.
Resolution
Smardex Team: Acknowledged.
-
L-69 Low DoS By Occupying The Validator Griefing Acknowledged
Description
When a user initiates an action and the given validator already has an pending action the call will revert. This can be misused by malicious actors as a DoS attack by front-running other user's transaction and occupying the given validator.
Recommendation
Allow users to disable that other users can set their address as the validator.
Resolution
Smardex Team: Acknowledged.
-
L-70 Low Bad Debt Not Handled In Validate Open Pos DoS Resolved
Description
The validate open position flow does not handle bad debt in the
_validateOpenPositionUpdateBalancesfunction (does not cap the values at 0). This can lead to an underflow and therefore to a long term DoS as this is a validation function.Recommendation
Handle bad debt in the validate open position flow.
Resolution
Smardex Team: The issue was resolved in PR#708.
-
L-71 Low Ether Griefing Attack Logical Error Resolved
Description
When a user inits an action and use a different account as validator and this validator has a stale pending open position action, the action is removed and the security deposit is sent to this validator.
This enables a griefing attack for a malicious validator if there is another stale pending open pos action in the system:
- User inits a new action and uses a malicious validator
- The validator has a stale pending open pos action and the security deposit is sent to the validator
- The validator receives the funds and reenters the system by calling
refundSecurityDeposit - The
refundSecurityDepositfunction transfer the security deposit value of another pending open pos
action to another validator and decreases the balance of the contract by doing so
- The init call of the user goes on and as the balance of the contract decreased by the security
deposit in the step before, the users will lose 0.5 ether in the
_refundExcessEtherflow when the current balance of the contract is compared with the one at the start of the init callRecommendation
Add a reentrancy guard to the
refundSecurityDepositfunction.Resolution
Smardex Team: The issue was resolved in PR#689.
-
L-72 Low Sandwich Remove Pending Open Pos MEV Acknowledged
Description
If a pending open position is removed with the
_removeBlockedPendingActionfunction and the position accrued bad debt, the bad debt is applied to the vault.LPs can see the transaction that would apply bad debt to the vault and front-run it to initiate a withdrawal before.
As the validate withdraw function uses the balance of the vault at init time the LP can avoid paying for the bad debt and socialize more debt to the other LPs by doing so.
Recommendation
This finding serves only to document this behavior. Be aware of these potentially unexpected behaviors and how they could be manipulated.
Resolution
Smardex Team: Acknowledged.
-
L-73 Low Fee Can Be Avoided By Depositing Dust Amts Validation Acknowledged
Description
There is no minimum deposit amount implemented in the system, therefore the fee can be avoided by depositing a tiny amount of ether so that the fee rounds down to zero.
This is unlikely to realistically be leveraged in production, however there may be additional unexpected behaviors which arise with small deposits.
Recommendation
Consider implementing a minimum deposit amount.
Resolution
Smardex Team: Acknowledged.
-
L-74 Low Traders Can Choose Prices In Edge Cases Validation Acknowledged
Description
In the
_getValidateActionPricefunction, if the_lowLatencyDelay(20mins) passed since the init, the chainlink price at init +_lowLatencyDelay(20mins) is used instead of the price at init +_validationDelay(24secs).Therefore users can wait and see if the price moves in their favor and depending on that decide if they execute it with the price at init + 24s or at init + 20m. The only mechanism that prevents this are MEV bots that should try to get the security deposit after init + 15m.
If for any reason this does not happen the user can choose between the two prices with a difference of 20 minutes.
Recommendation
Consider always using the validation price at init +
validationDelay.Resolution
Smardex Team: Acknowledged.
-
L-75 Low SDEX Burn Could Be Bypassed Validation Resolved
Description
The
transferCallbackfunction calls themsg.sendercontract and checks if theSDEXamt of theDEAD_ADDRESSincreased by the given amt after this call.If this protocol is deployed twice for example to enable a second asset, the user could re-enter on this callback in the second protocol instance and perform two deposits of equal worth by burning the SDEX amt needed only once and this check will pass as the
SDEXbalance of theDEAD_ADDRESSincreased by the needed amt (but only once instead of twice).Recommendation
Be aware of this potential issue if the protocol will be deployed multiple times in the future and consider transferring the assets to different dead addresses in that case.
Resolution
Smardex Team: The issue was resolved in PR#790.
-
L-76 Low Imbalance Checks Wrong In Edge Cases Validation Resolved
Description
The
_checkImbalanceLimitWithdrawalfunction passes if the calculatednewVaultExpois < 0 as theif(newVaultExpo == 0) { revert }check will pass and theimbalanceBpsvalue will be below 0 when it divides by the negativenewVaultExpovalue.It can be in edge cases that the
newVaultExpois below 0:- two LPs are in the system
- the first LP initiates a full withdrawal and the
_pendingBalanceVaultis decreased based on the
current state in the system
- before the withdrawal is validated bad debt is applied to the vault and therefore the
_balanceVault
is decreased
- the second LP initiates a full withdrawal therefore two withdrawal values are decreased from the
_balanceVaultin thenewVaultExpocalculation which are combined bigger than the_balanceVaultvalue, as the first withdrawal value saved in_pendingBalanceVaultwas calculated at a time where the vault hold more value than it does nowThis edge case allows to initiate a withdrawal for more funds than there are left in the vault, which will most likely lead to a long-term DoS as the validation call reverts. The same behaviour can be seen in the
_checkImbalanceLimitOpenfunction.Recommendation
Change the
if (newVaultExpo == 0) { revert }check toif (newVaultExpo <= 0) { revert }.Resolution
Smardex Team: The issue was resolved in PR#709.
-
L-77 Low Stepwise Jump In sdexToBurn Calc Logical Error Acknowledged
Description
Proof of concept: PoC
The
sdexToBurncalculation uses the estimatedUSDNtokens based on the saveddivisorinstead of the current state. As the saveddivisoris usually updated in intervals it can be stale at any time. This leads to a stepwise jump in the needed amount ofSDEXto burn. The higher thedivisorthe lessSDEXneeds to be paid.For example:
- block 1:
- Alice deposits 10
wstETH - block 2:
- divisor is updated
- block 3:
- Bob deposits 10
wstETH
In this example, Bob needs to burn more
SDEXthan Alice without any price changes inwstETHorSDEX. Therefore users can saveSDEXby depositing before adivisorupdate. Aside from burning too few fees, another issue can occur. If thepreviewDepositfunction is used to calculate the neededsdexToBurn_for a deposit and this amount is approved to the protocol an unexpected revert could happen if thedivisoris updated before the deposit is executed.For example by a
liquidatecall that ignores thedivisorinterval:- User calls
previewDepositand approves the returnedsdexToBurn_amount to the protocol - The User creates a
deposittransaction - A liquidator calls
liquidatewith more gas in the same block - The
liquidatecall gets executed first and thedepositcall reverts as not enoughSDEXis approved.
This can lead to unexpected reverts from time to time.
Recommendation
Options that can be considered to resolve this are:
- Calculate the current divisor and use it to calculate the
sdexToBurn_ - Ignore the
divisorinterval not only inliquidatebut also in thedepositflow - Calculate the
sdexToBurn_fee based on the value of the deposited asset instead of theUSDN
tokens to mint
Resolution
Smardex Team: Acknowledged.
-
L-78 Low Rebalancer tradingExpoToFill Not Entirely Filled Unexpected Behavior Acknowledged
Description
In the
_calcRebalancerPositionTickfunction if theliquidationPenaltystored on the liquidation tick of the new rebalancer position does not match thecurrentLiqPenaltythen theliqPriceWithoutPenaltyis adjusted to use the correct liquidation penalty for that tick. This may result in a lower unpenalized liquidation price when the penalty stored on the tick is higher than the current penalty.This results in a lower leverage and lower exposure for the resulting rebalancer position than was desired. Ultimately this means the entire
tradingExpoToFillwill not be met as the actual liquidation price has been reduced from theidealLiqPricewhich corresponds to thetradingExpoToFill. In this case the following if case will trigger in order to fill the gap to thelongImbalanceTargetBpsby incrementing the liquidation tick by a tick spacing.However in the case where the
highestUsableTradingExpowas used then this case will not trigger as it requires thatdata.highestUsableTradingExpo != tradingExpoToFill. This assumes that thehighestUsableTradingExpohas been entirely used by the new rebalancer position, but this may not be the case in the edge case where the liquidation penalty is higher than expected.When this edge case occurs the if case should trigger when there is a significant dearth from the
highestUsableTradingExpo. This way thelongImbalanceTargetBpshas a higher chance of being reached when thetradingExpoToFillwas not met in thesehighestUsableTradingExpocases.Recommendation
Consider changing the
data.highestUsableTradingExpo != tradingExpoToFillrequirement for the tick spacing increment case to be based upon how much exposure is actually taken up by the new rebalancer position. The most exact validation would be to compare thehighestUsableTradingExpoto the exposure of the position after it has been moved up by one tick to see if this increment can reasonably occur within the max leverage.This would be implemented in pseudocode as:
positionExposureIncrement = 0.0202 * currentPrice * liquidationPriceWithoutPenalty / ((currentPrice - liquidationPriceWithoutPenalty) * (currentPrice - 1.0202 * liquidationPriceWithoutPenalty)) if (data.highestUsableTradingExpo < posData_.totalExpo + posData_.totalExpo * positionExposureIncrement && ...){...}It's worth noting that this would not account for a change in the liquidation penalty at the new tick. This exact validation is likely too complex to be worth implementing, but serves as an example to show ho this edge case might be addressed.
In practice a less accurate validation could be acceptable, or this issue may be simply acknowledged as an acceptable inaccuracy of the rebalancer.
Resolution
Smardex Team: Acknowledged.
-
L-79 Low Funding Is Charged During Paused States Logical Error Resolved
Description
Funding fees are charged in order to incentivize users to balance the protocol. However, when the system is in a paused state, these fees are still being charged. When the system is unpaused and a recent price is provided, funding will be charged to the entire paused elapsed time.
Recommendation
Freeze funding fees during paused states. It can be achieved via applying funding when pausing the protocol and updating the last timestamp when unpausing.
Resolution
Smardex Team: The issue was resolved in PR#678.
No findings match.
Invariants 57
The review's fuzzing suite asserted 57 invariants. 55 held and 2 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
GLOB-01 | A positions tick should never be above the _highestPopulatedTick | Held |
GLOB-02 | The current divisor should never equal the MIN_DIVISOR.” | Held |
GLOB-03 | FundingPerDay should never equal 0 | Held |
GLOB-04 | Each pending action should have an associated securityDeposit value. | Held |
GLOB-05 | Trading expo should never go to 0. | Held |
GLOB-06 | Position should never have a leverage smaller that 1. | Held |
GLOB-07 | The internal total balance the contract deals with should not be bigger than the real | Held |
ERR-01 | balanceOf. Non-whitelisted error should never appear in a call | Broken |
DEPI-01 | Sender's ETH balance decreased by security deposit | Held |
DEPI-02 | Sender's wstETH balance decreased by deposited amount | Held |
DEPI-03 | Sender's SDEX balance decreased | Held |
DEPI-04 | Protocol's ETH balance increased by security deposit | Held |
DEPI-05 | Protocol's wstETH balance increased by deposit minus pending actions | Held |
DEPV-01 | Recipient's USDN shares increased after validation | Held |
DEPV-02 | Caller’s USDN shares unchanged | Held |
DEPV-03 | Validator’s USDN shares unchanged | Held |
DEPV-04 | Validator's ETH balance increased by security deposit after validation | Held |
DEPV-05 | USDN token total supply changed by pending tokens after validation | Held |
DEPV-06 | Caller’s wstETH balance unchanged | Held |
DEPV-07 | Protocol's wstETH balance decreased by pending actions after validation | Held |
DEPV-08 | Validator's wstETH balance unchanged | Held |
DEPV-09 | Caller’s wstETH balance unchanged | Held |
WITHI-01 | Sender's ETH balance decreased by security deposit | Held |
WITHI-02 | Sender's USDN shares decreased by withdrawn amount | Held |
WITHI-03 | Protocol's ETH balance increased by security deposit minus last action's deposit | Held |
WITHI-04 | Protocol's USDN shares increased by withdrawn amount plus pending actions | Held |
WITHV-01 | Sender's ETH balance increased by action's security deposit value | Held |
WITHV-02 | If successful, sender's wstETH balance increased or remained the same | Held |
WITHV-03 | If successful, protocol's ETH balance decreased by action's security deposit value | Held |
WITHV-04 | If successful, protocol's USDN shares decreased | Held |
WITHV-05 | If successful, protocol's wstETH balance decreased by at least pending actions | Held |
POSOPNI-01 | Protocol's ETH balance increased by security deposit minus last action | Held |
POSOPNI-02 | Sender's ETH balance decreased by security deposit minus last action | Held |
POSOPNI-03 | Protocol's wstETH balance increased by deposit amount minus pending actions | Held |
POSOPNI-04 | Sender's wstETH balance decreased by deposit amount | Held |
POSCLOSI-01 | If successful, sender's ETH balance decreased by security deposit | Held |
POSCLOSI-02 | If successful, validator's pending action is set to ValidateClosePosition | Held |
POSCLOSI-03 | If successful, protocol's ETH balance increased by security deposit | Held |
POSCLOSI-04 | Sender's wstETH balance unchanged | Held |
POSCLOSI-05 | Protocol's wstETH balance unchanged | Held |
POSOPNV-01 | If successful, validator's ETH balance increased by security deposits | Held |
POSOPNV-02 | If successful, protocol's ETH balance decreased by security deposits | Held |
POSOPNV-05 | Protocol's wstETH balance decreased by pending actions | Held |
POSOPNV-06 | Sender's wstETH balance unchanged | Held |
POSCLOSV-01 | If successful, sender's ETH balance increased by security deposit | Held |
POSCLOSV-02 | If successful, protocol's ETH balance decreased by security deposit | Held |
POSCLOSV-03 | If successful, protocol's wstETH balance decreased by more than pending | Held |
POSCLOSV-04 | actions If successful, protocol's wstETH balance decreased by less than close amount + | Held |
POSCLOSV-05 | pending actions If successful, recipient's wstETH balance increased by less than close | Held |
POSCLOSV-06 | amount If successful, recipient's wstETH balance increased | Broken |
POSCLOSV-07 | If successful and sender != validator, validator's ETH balance unchanged | Held |
POSCLOSV-08 | If successful and sender != recipient, sender's wstETH balance unchanged | Held |
POSCLOSV-09 | If successful and recipient != validator, recipient's ETH balance unchanged | Held |
POSCLOSV-10 | If successful and recipient != validator, validator's wstETH balance unchanged | Held |
PENDACTV-01 | Correct number of actions validated | Held |
PENDACTV-02 | Sender's ETH balance increased by security deposit | Held |
PENDACTV-03 | Protocol's ETH balance decreased by security deposit | Held |
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
