Impermax engaged Guardian to review the security of the Impermax V3 system allowing for tokenized LP collateral. From the 2nd of January to the 9th of January, a team of 6 auditors reviewed the source code in scope.
- Published
- Review window
- January 2 to 9, 2025
- Language
- Solidity
- Chains
- Arbitrum
- Sector
- Lending
- 1 Critical
- 3 High
- 6 Medium
- 22 Low
- 0 Informational
Scope
Overview
Impermax engaged Guardian to review the security of the Impermax V3 system allowing for tokenized LP collateral. From the 2nd of January to the 9th of January, a team of 6 auditors reviewed the source code in scope.
Issues Detected Throughout the engagement 4 High/Critical issues were uncovered and promptly addressed by the Impermax team.
Security Recommendation Given the number of High and Critical issues detected as well as additional findings reported during remediation review, Guardian recommends that an independent security review of the protocol at a finalized frozen commit is conducted before deployment. Furthermore, Guardian recommends the testing infrastructure to be expanded to include all user flows such as the reinvest functionality. The developed fuzzing suite can be used to port over the codebase to Foundry from Truffle.
Findings 32
-
C-01 Critical Collateral-Free Borrows Reentrancy Resolved
Description
Currently the
borrowfunction is at risk of cross-contract reentrancy. Consider the following scenario:(1)
ImpermaxV3Borrowable.borrowwithborrowAmount > 0with an NFTLP collateral that is enough to cover the borrow.(2) Callback
impermaxV3Borrowis called before tokenId's borrow balance is updated. At this point in time, the account's borrowed is 0. (3) CallImpermaxV3Collateral.redeemwithpercentage=1e18to get back the NFTLP. This is still in the context of the callback.(4)
require(IBorrowable(borrowable0).borrowBalance(tokenId) = 0andrequire(IBorrowable(borrowable1).borrowBalance(tokenId) = 0should pass successfully as the account wasn't updated yet.(5) Back in
ImpermaxV3Borrowable.borrow,canBorrowwould be called, but the NFTLP position still has enough liquidity to cover the borrow and thegetPositionDatafunction would still return the proper values, even if Alice now holds the NFT instead of theImpermaxV3Collateralcontract.(6) Ultimately, Alice borrowed a non-zero amount but also holds the entirety of her collateral NFTLP.
Recommendation
Ensure the
ImpermaxV3Collateralcontract holds the NFTLP collateral when there is an active borrow.Resolution
Impermax Team: The issue was resolved in commit d71e5b6.
-
H-01 High Auto-Compounding Can Lead To Liquidations Validation Partially resolved
Description
Proof of concept: PoC
The auto-compounding feature should help borrowers be more capital efficient by automatically reinvesting fees earned on uniswap. As the caller of the
reinvestfunction receives a bounty for doing the work, the position's collateral value decreases through auto-compounding.The
reinvestfunction does not implement safety checks for the position's health. Therefore, it is possible that calling thereinvestfunction leads to positions being liquidatable, which is definitely not in the borrower's interest and should therefore be prevented.It is also possible to auto-compound liquidatable positions and therefore potentially make them go underwater.
Recommendation
Revert at the end of the
reinvestflow if the position is liquidatable. It would also make sense to let borrowers decide up to which point they want to auto-compound so that reinvestors can not push them to the edge of liquidation.Resolution
Impermax Team: The issue was resolved in commit af10809.
-
H-02 High Oracle Price DoS DoS Resolved
Description
The function
oraclePriceSqrtX96aims to calculate the oracle price by gathering the TWAP across all Uniswap pools with a specific (token0, token1) pair and calculating the mean.The pre-condition for a successful call to
oraclePriceSqrtX96is that for each Uniswap pool in thepoolsList, theconsultfunction does not revert.However, if a pool was just created, there would not be an observation that was
ORACLE_Tseconds ago and Uniswap'sobserveSinglewould revert: "dev Reverts if an observation at or before the desired observation timestamp does not exist"Ultimately, a user can DoS the calculation of oracle pricing for an entire period just by deploying a different fee tier, which would prevent
positionDataretrieval and DoS other areas of the codebase such as functionisUnderwaterand consequentlyrestructureBadDebt.Recommendation
Consider wrapping the call to Uniswap's
observein a try-catch and then adjusting the number of observations by how many calls were successful.Resolution
Impermax Team: The issue was resolved in commit 6f6ab04.
-
H-03 High Restructure Debt Locks Up Funds Logical Error Resolved
Description
When a position’s debt is restructured, the debt is forgiven, the losses socialized, and the position remains in the system. In certain cases, the position can become liquidatable after restructuring and be liquidated, and other times the position can become fully healthy and remain.
This incongruent behavior may be unexpected for the protocol, since some positions will effectively get a bail-out and remain. This can be used by malicious borrower’s to keep lender’s funds locked up indefinitely.
Recommendation
Consider if previously underwater positions should remain in the system after restructuring and allow them to be liquidated.
Resolution
Impermax Team: The issue was resolved in commit 92796e2.
Guardian Team: The owner of a position can frontrun the liquidator to call
restructureBadDebtfirst so that the position is healthy again and then stuff the block so that the block gas limit is hit before the liquidator's transaction goes through. Afterwards theblockOfLastRestructureOrLiquidationcheck will fail. -
M-01 Medium Arbitrary NFTLP Code Can Be Used Validation Acknowledged
Description
The
ImpermaxFactorycontract allows for the creation of lending pools with a completely arbitrary NFTLP address. Consequently, a malicious user may create a lending pool with a malicious tokenized position which could lead to loss of user data and assets.Recommendation
Validate that the NFTLP's passed into the
ImpermaxFactoryfunctions have been deployed through the respective tokenized position factory contracts.Resolution
Impermax Team: Acknowledged. Validation is done through the whitelisting process at a higher level (generally in the UI).
-
M-02 Medium Interest Lost On Low Decimal Tokens Rounding Acknowledged
Description
In the
accrueInterestfunction, the interest calculationinterestFactor.mul(_totalBorrows).div(1e18)can round down to zero for low-decimal tokens (such as USDC), particularly when the borrow rate or total borrows are low.Specifically, when the product of
interestFactorand_totalBorrowsis less than 1e18, the division results in zero. This leads to a loss of interest for lenders, as theinterestAccumulatedis zero.Furthermore, this can cause a discrepancy between
totalBorrowsandaccountBorrows, since accountBorrows does not experience the same precision loss.Recommendation
Use a higher precision for the rate values to accommodate tokens with fewer decimals, ensuring that the computed interest does not round to zero.
Resolution
Impermax Team: Acknowledged.
-
M-03 Medium Borrow Rate Gaming Logical Error Acknowledged
Description
The
currentBorrowBalanceandtrackBorrowfunctions accrue interest and therefore update thetotalBorrowswhich would influence theutilizationRateand therefore theborrowRate. However, they do not have the_updatemodifier and therefore do not update theborrowRate.Therefore the system potentially deals with a stale
borrowRateand the lenders receive less yield than they should. Furthermore, the system allows for interest gaming, since callingsync()every second will provide a different debt accumulated than callingsync()once over the same timespans.For example, with a decreasing
_kinkBorrowRateevery sync the borrow rates will also decrease and cause less to be repaid over the borrow’s lifetime, which in turn is less yield for lenders.Recommendation
Add the
_updatemodifier to thecurrentBorrowBalanceandtrackBorrowfunctions. Furthermore, re-consider if the system show allow for varying sync times to lead to different costs on users.Resolution
Impermax Team: Acknowledged. We call calculateBorrowRate when there is a supply/demand change in the contract.
-
M-04 Medium Interest Not Accumulated Before Admin Changes Logical Error Acknowledged
Description
Interest should always be accrued before updating parameters that will impact the outcome of the next update to fairly calculate the accumulated interest.
The
_setAdjustSpeedfunction in theBSettercontract for example can update theadjustSpeedvariable that will influence theborrowRate, but it does not accrue interest before updating this variable.Therefore the next time the
borrowRateis updated it applies the updatedadjustSpeedvalue on the wholetimeElapsedsince the last update while in reality a part of thetimeElapsedshould be calculated with the oldadjustSpeedvalue to fairly update theborrowRate.Recommendation
Accrue interest before updating parameters that will impact the outcome of interest related calculations.
Resolution
Impermax Team: Acknowledged.
-
M-05 Medium ERC-777 Reentrancy In reinvest Logical Error Resolved
Description
When the bounty is sent to the given
bountyToaddress with an ERC-777 token the receiver can re-enter the system. At this point, the fee growth values of the position were already reset, but the position had not received the liquidity yet.Therefore the collateral value of the position is smaller than it is in reality. This could lead to the system seeing the position as liquidatable or underwater when the caller reenters the system.
Therefore a malicious actor can potentially abuse this state to either:
- Liquidate the position to steal from its owner
- Call
restructureBadDebtto reduce the debt of the position and steal from the lenders (could be
abused by the owner of the position)
Recommendation
Transfer the bounty to the given
bountyToaddress after all state changes occurred.Resolution
Impermax Team: The issue was resolved in commit 33126d8.
-
M-06 Medium Frontrun Restructure Bad Debt Frontrunning Acknowledged
Description
When
restructureBadDebtis called, theexchangeRatewill immediately decrease due to the reduction in_totalBalancewithout a proportional reduction in pool token supply. A user can frontrun the restructuring to redeem before the exchange rate drop to avoid the penalty.The LP exploiting this receives yield without risk while increasing the risk of all the other LP as the bad debt is socialized among all other LPs. Furthermore, the LP taking advantage of the stepwise jump in the exchange rate can then mint the same amount of
PoolTokensfor a fraction of the price.Recommendation
Consider a 2 step deposit/redeem process.
Resolution
Impermax Team: Acknowledged. I think the only real solution to this would be to introduce an unbonding period for redeem. But this would ruin the UX for a marginal benefit.
-
L-01 Low Liquidators Receive Unexpected Values Logical Error Acknowledged
Description
The
ImpermaxV3Collateralcontract utilizes a 30-minute TWAP to determine the value of collateral and debt during the liquidation process. This approach helps provide a stable price by averaging out short-term market fluctuations.However, when the liquidated tokens are claimed, Uniswap uses the spot price. This discrepancy between the TWAP and the spot price can lead to differences in the expected versus actual value received by liquidators.
For instance, if the spot price is lower than the TWAP, a liquidator may receive fewer tokens than anticipated, resulting in a possible loss.
Recommendation
Clearly document potential discrepancies between TWAP and spot pricing so users understand the associated risks
Resolution
Impermax Team: Acknowledged. Since liquidations will be handled by external contracts, all the PNL logic and liquidations strategy will be handled by the liquidator.
-
L-02 Low Unnecessary Rounding Increment Logical Error Resolved
Description
In the
joinfunction 1 wei is added to the resultingnewFeeGrowthInside0LastX128of the joined position to avoid fee over estimations.However in cases where the
newFeeGrowthInside0LastX128result does not have precision loss this is an unnecessary addition. Instead rounding up can occur only when precision loss would occur.Recommendation
Consider implementing
tA0.add(tB0).add(newLiquidity - 1).div(newLiquidity)Instead of the existingtA0.add(tB0).div(newLiquidity).add(1)to accurately only round up when precision loss would occur.Resolution
Impermax Team: The issue was resolved in commit 5d38704.
-
L-03 Low Safety Margin Updates Causes Liquidations Warning Acknowledged
Description
The
safetyMarginSqrtplays a crucial role when determining the liquidatable state of a position, as it's used to calculate theLOWESTandHIGHESTprice as a factor of the oracle TWAP price.Therefore, healthy position can become instantly liquidatable if this factor is increased by the admin. The same issue applies when updating the
liquidationIncentiveandliquidationFee.Recommendation
Introduce a time lock feature so that these admin state changes include a delay, to allow users to take action before they are applied.
Resolution
Impermax Team: Acknowledged.
-
L-04 Low Liquidate Can Be Called With Zero Repay Validation Acknowledged
Description
Functions throughout the codebase allow for an input of zero, e.g. functions
ImpermaxV3Borrowable.liquidateforrepayAmountandImpermaxV3Collateral.redeemforpercentage.This would ultimately leads to events being emitted when no actual actions have been taken on a Position.
Recommendation
Consider validating against zero inputs.
Resolution
Impermax Team: Acknowledged.
-
L-05 Low Functions Naming Convention Best Practices Acknowledged
Description
There are multiple public/external functions with names starting with an underscore. Some examples:
TokenizedUniswapV3Factory._setPendingAdminCSetter._initializeBSetter._setReserveFactor
However, this naming style goes against the solidity convention for external/public vs internal/private functions, more details here:
https://docs.soliditylang.org/en/latest/style-guide.html#underscore-prefix-for-non-external-function
Additionally, this can create confusion for the reader, or allow bugs to be introduced, declaring a function external when it's suppose to be private/internal.
Recommendation
Consider adapting to the function naming convention, according to the solidity docs shared above.
Resolution
Impermax Team: Acknowledged.
-
L-06 Low Incompatible Types Warning Resolved
Description
Within the
IUniswapV3AC01the interface there are numerous functions where the return type does not match the expected return type.For example,
function PROTOCOL_SHARE() external view returns (address);expected an address to be returned in the interface definition but thePROTOCOL_SHAREis a uint.Recommendation
Correct the interface definitions so the return types match.
Resolution
Impermax Team: The issue was resolved in commit 8a88634.
-
L-07 Low Black Swan Events Lead To Value Theft Validation Acknowledged
Description
- The
borrowfunction allows to borrow up to the point of being liquidatable therefore a tiny price
change afterward will make the position liquidatable
- The system uses TWAP prices at the moment to calculate the value of collateral and debt
This could be problematic in black swan events when prices drop or rise very quickly. It could lead to a situation where the TWAP price is off by more than the safety margin + liquidation fees compared to the current price.
In this case, anyone would be able to borrow funds that are worth more than the deposited collateral, use these funds to buy more collateral, and repeat the process to drain the protocol.
Recommendation
There are multiple things that can decrease the likelihood of this edge case:
- Add a percentage buffer on top of the liquidation threshold so that users are not able to borrow up
to the point of being liquidatable
- Use a different price oracle that would be more accurate in such an edge case
Resolution
Impermax Team: Acknowledged.
- The
-
L-08 Low Multiple NFTLPs Owned By ReservesManager Logical Error Acknowledged
Description
During liquidations, the NFTLP token will we split twice, one for the liquidator, one for the
reservesManager. Even though eachtokenIdcan be in different ranges, thereservesManagerwill end up with hundreds of NFTLPs that will later need to be redeemed to collect the underlying.Recommendation
Consider redeeming the NFTLP to receive the underlying tokens.
Resolution
Impermax Team: Acknowledged.
-
L-09 Low Invalid Price Validations Validation Resolved
Description
UniswapV3CollateralMath.newPositionvalidates thatpaSqrtX96 > Q32andpbSqrtX96 < Q160However, Q32 is below Uniswap'sMIN_SQRT_RATIOand Q160 is above Uniswap'sMAX_SQRT_RATIO, which leads to potentially invalid position objects being created.Recommendation
Align the validation with Uniswap's min and max prices.
Resolution
Impermax Team: The issue was resolved in commit 672084e.
-
L-10 Low Protocol Revenue Affected By Liquidations Logical Error Acknowledged
Description
The current implementation of
ImpermaxV3Borrowable.liquidateallows users to partially repay the current borrowed balance. However, depending on the previous health state, a partial repay can make the position healthy post liquidation.This means that both the protocol ad the user could have received more fees during a full liquidation.
If the user has both tokens borrowed from the pool, and based on the liquidity structure of the position, there are cases where repaying one of the tokens first will allow the liquidator to trigger a second liquidation with the other token.
Recommendation
Protocol should be aware of the fees they are missing out with partial liquidations. Additionally, document this behavior, so users are aware of the impact of partial liquidations.
Resolution
Impermax Team: Acknowledged. Partial liquidations make us miss out on fees, but they make the protocol safer.
-
L-11 Low Redundant Function Calls In ImpermaxV3Factory Best Practices Resolved
Description
The
createCollateral,createBorrowable0, andcreateBorrowable1functions call the_getTokensfunction in the beginning without using the function's return values. Therefore, the call is redundant and can be removed.Recommendation
Remove the redundant function calls. If there should be a check to ensure the two token addresses are set add a require statement instead.
Resolution
Impermax Team: The issue was resolved in commit 74c85cf.
-
L-12 Low Repay Window Created By TWAP Logical Error Acknowledged
Description
Due to the lagging nature of TWAP price, borrowers will have a time window to repay their loans, once they notice the price went against them, but the TWAP price has not catch up yet.
Therefore, liquidators will have a hard time finding liquidatable positions, as users will know ahead of time due to the pool price action.
Recommendation
Document this behavior to the users and liquidators.
Resolution
Impermax Team: Acknowledged.
-
L-13 Low Zero Liquidity Positions Are Permitted Validation Acknowledged
Description
There is no zero check in the
mintfunction of theTokenizedUniswapV3Positioncontract therefore zero liquidity positions are permitted.It is also possible to split 0% of a position to create a new position with 0 liquidity. Such unexpected states should not occur and might lead to vulnerabilities with future updates.
Recommendation
Add zero checks to prevent the creation of zero liquidity positions.
Resolution
Impermax Team: Acknowledged. Through the router it’s easier to create a leveraged position by first minting a position with 0 liquidity.
-
L-14 Low Missing Reinvestor Protection Validation Acknowledged
Description
If multiple people call
reinvestat the same time, the transactions of the users who paid less gas might be successful and the users have to pay the full gas costs, but the transaction does not have any necessary effect and the users will not receive a bounty:- Bob calls
reinvest - Alice calls
reinvest - Bob paid more gas and the transaction went through first
- The position's fees are reinvested
- Bob receives a bounty for his work
- Alice's transaction went through and in between some fees were accrued on Uniswap
- A dust amount or even no fees are reinvested
- Alice does not receive a bounty or the bounty that does not compensate for her gas costs
This does also allow a griefing vector, a malicious actor could front-run the
reinvestcall on his position andsplittheNFTLPinto two positions so that thereinvestcaller wastes gas toreinvesta dust amount of fees.Recommendation
Add a
lastReinvestslippage check at the beginning of thereinvestfunction, or document that this function should be called over a contract that implements such a check.Resolution
Impermax Team: Acknowledged.
- Bob calls
-
L-15 Low Direct Interactions May Risk User Funds Frontrunning Acknowledged
Description
The protocol relies on a two-step process for most of its depositing functionality. Users send funds to the contract or a Uniswap position first, and then call a function that mints the corresponding tokens or positions.
This works well for those who use the periphery contracts, since both actions occur atomically. However, users who interact directly with the protocol’s core contracts may risk losing their funds if, for instance, the second transaction is frontrun.
Recommendation
Make it clear in the documentation that this risk exists and advise users to use the protocol’s periphery contracts.
Resolution
Impermax Team: Acknowledged. No user should ever interact with the core directly.
-
L-16 Low Inconsistent Split Percentages Validation Resolved
Description
TokenizedUniswapV2Positionprevents a split percentage of 100% butTokenizedUniswapV3Positionallows it.Recommendation
Consider making the validation symmetrical.
Resolution
Impermax Team: The issue was resolved in commit 2b519ec.
-
L-17 Low NFTLP positions Not Compatible With Router Logical Error Acknowledged
Description
The current implementation of
TokenizedUniswapV3Positioncontains aPositionstruct that stores position data bytokenId. However, this struct will not be compatible with the periphery contracts, and theunclaimedFees0andunclaimedFees1are unexpected returned params.Recommendation
Be aware of this issue, and update the periphery contracts and avoid integration issues
Resolution
Impermax Team: Acknowledged. We are aware of this, we will update the periphery once the audit for the core is completed.
-
L-18 Low Unsafe Mint Function Used Validation Resolved
Description
The ERC721
mintfunction is used multiple times in the codebase instead of thesafeMintfunction. If the receiver is a contract and is not capable of handling NFT-related actions and/or calling other functions in the system, the NFTs will be stuck.Recommendation
Consider using the
safeMintfunction instead of themintfunction to make sure the receiver is capable of handling NFTs.Resolution
Impermax Team: The issue was resolved in commit 44b3100.
-
L-19 Low Split NFT Receiver Is Always The Owner Logical Error Acknowledged
Description
When calling the
splitfunction in theTokenizedUniswapV3Positioncontract, the receiver of the new NFT is always the owner.The function call could come from an approved actor who wants to receive a part of the NFT which is not possible due to this hardcoded behavior
The approved actor is therefore allowed to receive the whole NFT but not a part of it.
Recommendation
Add a
toparameter to thesplitfunction as in themintfunction.Resolution
Impermax Team: Acknowledged.
-
L-20 Low Lack Of Minimum Borrow Size Validation Acknowledged
Description
Currently there isn't a minimum on the size of the borrow, which would require a very small collateral amount. In such a case, there may be not enough incentive for a liquidator to liquidate the borrower, and the protocol risks accumulation of bad debt.
Recommendation
Consider enforcing a minimum borrow size.
Resolution
Impermax Team: Acknowledged.
-
L-21 Low DoS Between Liquidatable & Underwater DoS Acknowledged
Description
A position is not liquidatable if the position is
underwater(has acquired bad debt). At this point anyone needs to callrestructureDebtto forgive the debt and make the position liquidatable again. This can lead to DoS when prices are around the position's bad debt threshold and fluctuate slightly.A liquidator could call
liquidateand between the creation of the transaction and its execution, the position could gounderwateras the prices changed slightly and theliquidatecall reverts.Then the liquidator would call
restructureDebtbut between the creation of the transaction and its execution the position could be healthy enough again to be liquidated without forgiving debt as the prices changed slightly again causing therestructureDebtcall to revert.This is not a big problem but it could be a bad user experience and delay the liquidation process, a process that should happen as fast as possible to avoid further damage.
Recommendation
Merge the
restructureDebtandliquidateflows into one function, so that therestructureDebtflow is executed if the position isunderwaterbefore continuing with theliquidateflow.Resolution
Impermax Team: Acknowledged. Before calling liquidate, the liquidator contract can check if the position is underwater. If it is, it can call
restructureDebtbefore calling liquidate. -
L-22 Low Configuration Leads To Gaming Configuration Acknowledged
Description
According to the CSetter, the minimum safety margin
SAFETY_MARGIN_SQRT_MINis 1e18.With this setting, a user can borrow up to about ~1x post-liquidation collateral ratio and then become both liquidatable and underwater simultaneously, introducing bad debt into the collateral very quickly.
This also differs from the whitepaper constraints that the
safetyMarginis between 150% and 250%.Furthermore, in the case that the liquidation fee is zero, a liquidatable position owner can self-liquidate their position to take advantage of the discount instead of repaying their borrow, since self-liquidation would require less collateral for the same amount of assets due to the
liquidationIncentive.Recommendation
Firstly, adjust the minimum sqrt safety margin validation according to the whitepaper. Secondly, ensure that a liquidation fee is active.
Resolution
Impermax Team: Acknowledged.
No findings match.
Invariants 9
The review's fuzzing suite asserted 9 invariants. 7 held and 2 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
COLL-01 | TokenizedUniswapV3Position.redeem should never revert | Held |
LIQUI-01 | After a successful call to restructureBadDebt function, the position should be liquidatable | Held |
LIQUI-02 | After a successful reinvest call position should not be liquidatable | Broken |
LIQUI-03 | After a successful call to restructureBadDebt function, the position should NOT be underwater. | Held |
BORROW-01 | After borrow the user's position is never liquidatable | Held |
BORROW-02 | After borrow the user's position is never underwater | Held |
BORROW-03 | After borrow, the borrowBalance must increase by the borrowAmount | Held |
BORROW-04 | When borrowAmount is 0, borrowedBalance should remain unchanged | Held |
GLOB-01 | Positions should always have > 0 liquidity | Broken |
More from Impermax
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.
