Sentiment engaged Guardian to review the security of its leveraged lending protocol, allowing for permissionless SuperPool and Pool creation. From the 17th of June to the 27th of June, a team of 7 auditors reviewed the source code in scope.
- Published
- Review window
- June 17 to 27, 2024
- Language
- Solidity
- Chains
- Arbitrum, Ethereum
- Sector
- Lending
- 5 Critical
- 19 High
- 23 Medium
- 15 Low
- 0 Informational
Scope
Overview
Sentiment engaged Guardian to review the security of its leveraged lending protocol, allowing for permissionless SuperPool and Pool creation. From the 17th of June to the 27th of June, a team of 7 auditors reviewed the source code in scope.
Issues Detected Throughout the engagement 24 High/Critical issues were uncovered and promptly remediated by the Sentiment team. Several issues impacted the fundamental behavior of the protocol, following their remediation Guardian believes the protocol to uphold the functionality described for the lending protocol product.
Security Recommendation Given the number of High and Critical issues detected, Guardian supports an independent security review of the protocol at a finalized frozen commit. Furthermore, the Sentiment team should increase testing with a variety of share valuations, collateral and borrow assets, and LTV ratios. The engagement exposed multiple blind spots that should be thoroughly tested and presented numerous opportunities for system malfunction.
Findings 62
-
C-01 Critical Incorrect Amounts Are Used During Withdrawals Validation Resolved
Description
Proof of concept: PoC
When assets are withdrawn from SuperPool, the
redeemfunction in the base pool is invoked. This function requires the share amount as an input. However, during this call, the asset amount is provided instead, leading to significant accounting issues and potential loss of funds.Recommendation
To address this issue, it is recommended to convert the user-provided asset amount to the corresponding share amount before proceeding with the redemption process.
Resolution
Sentiment Team: The issue was resolved in PR#222.
-
C-02 Critical Free Borrowing When Collateral Is The Same Asset Logical Error Resolved
Description
Proof of concept: PoC
Users deposit collateral and borrow assets using the PositionManager contract, and every action that changes a Position’s balance requires a health check via RiskEngine and RiskModule.
The health check is done by comparing total debt of a position and total asset of a position. However, this health check is inaccurate when a borrowed asset and the collateral asset are the same. The RiskEngine accounts newly borrowed assets as user provided collateral, which causes the check to be incorrect.
A pool owner can
- Set the
ltvto 1 for the borrow asset. - Add the borrow asset as collateral asset to his position with
addToken - Borrow all assets from the pool without adding any collateral.
Resulting in all funds to be frozen for regular depositors.
Recommendation
Do not allow borrow asset and the collateral asset to be the same.
Resolution
Sentiment Team: Resolved.
- Set the
-
C-03 Critical DoS Pool By Allowing Excess Borrowing DoS Resolved
Description
Proof of concept: PoC
Users can borrow from the
Poolonly by using thePositionManager. Although users must deposit enough collateral to pass the health check after borrowing, thePooldoes not check if thepoolIdhas enough liquidity to support the amount borrowed.Any amount borrowed that exceeds the
poolIdliquidity, will effectively steal this liquidity from other pools, and total borrows will be greater than total assets.There are multiple impacts with this issue:
- pool redeems are DoS'ed when calculating:
uint256 assetsInPool = pool.totalAssets.assets - pool.totalBorrows.assets- lenders won't be able to redeem from other pools, as there is not enough assets in balance
- SuperPool
maxWithdrawreverts asgetLiquidityOfcalculation will underflow
Recommendation
Prevent positions from borrowing more assets than the liquidity of the
poolId.Resolution
Sentiment Team: The issue was resolved in PR#243.
-
C-04 Critical Drain All Pools With SuperPool As Collateral Logical Error Resolved
Description
Proof of concept: PoC
SuperPoolvault shares should have the same decimals as the underlyingASSET. The decimal value is set in the constructor. However, this is not the case, as depositing 1e18 assets will give you 1e36 shares.The issue relies on
_convertToShareswhen the first user deposits, wherelastTotalAssetsandtotalSupplyare 0, but it will multiply by10 ** DECIMALS:shares = assets.mulDiv(totalSupply() + 10 ** DECIMALS, lastTotalAssets + 1, rounding);Although users will be able to redeem the shares for the correct amount of tokens, there is a discrepancy between the
decimals()of the vault and the minted share units. Users will be able to use this vault token as collateral to borrow assets against. When calculating the asset value of the vault token inRiskModule, the value returned will be 1e18 times greater than expected.Therefore, users will be able to drain pools by borrowing all assets, as the collateral value is basically infinite:
vaultTokens(1e36) * priceInEth(1e18) / decimals(1e18) = 1e36 ether valueRecommendation
Implement the following:
- shares = assets.mulDiv(totalSupply() + 10 ** DECIMALS, lastTotalAssets + 1, rounding);
- shares = assets.mulDiv(totalSupply() + 1, lastTotalAssets + 1, rounding);
- assets = shares.mulDiv(lastTotalAssets + 1, totalSupply() + 10 ** DECIMALS, rounding);
- assets = shares.mulDiv(lastTotalAssets + 1, totalSupply() + 1, rounding);
Resolution
Sentiment Team: The issue was resolved in PR#240.
-
C-05 Critical Attacker Can Drain All Funds from a Pool Logical Error Resolved
Description
Proof of concept: PoC
The
_getMinReqAssetValuefunction in the RiskModule contract determines the minimum required asset value for a position to be considered healthy. This function is called by the isPositionHealthy function, which performs a health check after every action or series of actions taken by users to ensure their position remains healthy.The issue arises because the _getMinReqAssetValue function relies on the length of the position's positionAssets array for the inner loop when calculating the required asset value for a position to be healthy. However, a position does not need to have any assets added to its positionAssets list in order to perform a borrow.
As a result, a user could perform a borrow with no funds, and the health check performed at the end would still pass. This occurs because
_getMinReqAssetValuewould return zero, given that the position'spositionAssetsis empty, despite the position having open debt from the borrow. Consequently, a user could borrow all funds from a pool and transfer them to personal accounts, effectively draining the pool.Recommendation
Update the
_getMinReqAssetValuefunction to revert if the resultingminReqAssetValueis zero. This function is only called when debt exceeds zero, sominReqAssetValueshould almost always be greater than zero.One exception is when a position's value falls to zero, causing insolvency. In such cases, this fix could block the liquidation of bad debt, so an admin function should be implemented to handle these scenarios.
Resolution
Sentiment Team: The issue was resolved in PR#271.
-
H-01 High setFee Honeypot Attack Validation Resolved
Description
Proof of concept: PoC
SuperPoolowners can adjust the fee anytime instantly and no boundaries for the fee are set. This enables a honeypot attack:- Attacker creates a
SuperPoolwith a 1% fee - Users deposit into the pool
- The attacker sets the fee way above 100%
- A few seconds pass and the pending interest for the attacker is >= all assets in the contract
- Attacker withdraws all funds of the
SuperPool
Recommendation
Implement min/max values and a timelock for critical parameter updates like fees.
Resolution
Sentiment Team: The issue was resolved in PR#225.
- Attacker creates a
-
H-02 High Malicious RateModel Attack Validation Resolved
Description
Proof of concept: PoC
- Anyone can create a
BasePoolwith an arbitrary contract asRateModel - Anyone can create a
SuperPool - The owner of a
SuperPoolcan add a newBasePoolto aSuperPooland reallocate all funds to it any
time
These conditions allow the following attack:
SuperPoolowner creates aBasePoolwith a maliciousRateModeland mints some sharesSuperPoolowner adds the BasePool to the queue of theSuperPoolSuperPoolowner reallocates all funds to the maliciousBasePool- The malicious actor calls accrue on the
BasePooland the maliciousRateModelreturns that a lot of
tokens in interest were accrued
- The malicious actor withdraws all funds in the
BasePoolwith the few shares minted in the
beginning and the users of the
SuperPoollose everythingRecommendation
Do not allow arbitrary addresses as
RateModel.Resolution
Sentiment Team: The issue was resolved in PR#264.
- Anyone can create a
-
H-03 High Protocol Fees Are Donations Logical Error Resolved
Description
- The
interestFeeandoriginationFeeare fees that go to the protocol. - Anyone can create a new
BasePooland set theinterestFeeandoriginationFeewhile doing so.
Therefore the protocol fees are not enforced and act like donations instead. As it makes no economic sense for the
BasePoolowner to set these fees above 0 the protocol will probably lose a lot of money.Recommendation
Enforce the protocol fees.
Resolution
Sentiment Team: The issue was resolved in PR#224.
- The
-
H-04 High Reallocating Can Freeze Funds Validation Resolved
Description
Proof of concept: PoC
In the
reallocatefunction it is not checked if the given pools to deposit to are part of the deposit/withdraw queue. Therefore maliciousSuperPoolowners can deposit funds into a pool that is not in the queue, which will freeze funds for the users of theSuperPool. The owner could then spread on social media that users need to pay X amount of funds to unfreeze it (this could be even done with a contract). When the users pay the amount, the owner can unfreeze the funds by adding the new pool to the queue or reallocating the funds back to the original pool.Recommendation
Add a check to see if the pools are part of the deposit/withdraw queue.
Resolution
Sentiment Team: The issue was resolved in PR#231.
-
H-05 High DoS On Setting The LTV Of a Token To Zero Logical Error Resolved
Description
Proof of concept: PoC
- The owner of a
BasePoolcan set the LTV of a token to zero. - The
isPositionHealthyfunction will revert if a position holds a token in the asset list and this token
has an LTV of zero.
Therefore if the owner updates the LTV of a token to zero and any position holds this token any interaction with it through the
PositionManagerwill revert.This has multiple bad consequences:
- Borrowers debt increases but they are not able to repay
- Borrowers can not be liquidated
- Borrowers are not able to get their collateral back
- The owner of a base pool can create unliquidatable positions
Unliquidatable positions:
- Owner sets the LTV of a token above 0
- The owner creates a position adds this token as collateral, borrows funds, and does something
risky with them
- The owner sets the LTV for this token to 0
- Now the position is not liquidatable till the owner sets the LTV back to a value above 0
Recommendation
Do not allow to set the LTV of a token to zero.
Resolution
Sentiment Team: The issue was resolved in PR#237.
- The owner of a
-
H-06 High _reorderQueue Does Not Work As Expected Logical Error Resolved
Description
Proof of concept: PoC
SuperPoolcontract has deposit and withdrawal queues. Order of these queues are important since deposits and withdrawals are done based on the order, which is supposed to be arranged by the owner.However, due to incorrect implementation of the
_reorderQueuefunction, the owner can not change queue orders. This function takes a new order as anindexesarray, and is supposed to rearrange the order based onindexes. But it only copies the previous order as is with thenewQueue[i] = queue[i]line.Recommendation
Use the inputted
indexesarray to determine new order. ChangenewQueue[i] = queue[i]tonewQueue[i] = queue[indexes[i]].Resolution
Sentiment Team: The issue was resolved in PR#220.
-
H-07 High DoS In _removePool By Force Feeding DoS Resolved
Description
- The
_removePoolfunction reverts if theSuperPoolstill owns assets in the given pool. - Anyone can deposit into a
BasePooland set anySuperPoolas the receiver of the assets.
This enables a malicious actor the possibility to front run a
_removePoolcall and deposit one wei of assets into theSuperPoolto DoS the call.Recommendation
Implement a function that reallocates and removes the pool in one transaction.
Resolution
Sentiment Team: The issue was resolved in PR#266.
- The
-
H-08 High Reentrancy In SuperPool Logical Error Resolved
Description
Proof of concept: PoC
The last three actions in the withdrawal flow of the
SuperPoolare:- Burning the share tokens
- Transferring the funds to the user
- Reducing the
lastTotalAssetsvalue
Therefore if the attacker reenters on receiving the tokens the total amount of shares is already reduced, but the total amount of assets is not.
The first action in withdraw/deposit is accruing interest calculated based on the saved
lastTotalAssetsvalue and the current total amount. Therefore the given difference on reentering (aslastTotalAssetsis not reduced yet) is seen as interest and the owner of the pool receives fees on this interest.This enables the following attack path:
SuperPoolowner deposits tokens into the own poolSuperPoolowner withdraws the tokens and reenters on receiving them over and over again- Every time the difference is seen as interest and the owner receives fees
- Owner withdraws the gained fees
- The owner repeats this process over and over again till the
SuperPoolis drained completely
Recommendation
safeTransfershould be the last action in this flow.- Use a reentrancy guard on critical functions.
Resolution
Sentiment Team: The issue was resolved in PR#244.
-
H-09 High Util Ratio Calculation Is Incorrect Logical Error Resolved
Description
Proof of concept: PoC
The
LinearRateModelcalculates thetotalAssetsamount by summing up thetotalBorrowsand theidleAssetAmt. But theidleAssetAmtalready is thetotalAssetsamount (pool.totalAssets.assetsfrom theBasePool). This leads to an incorrect calculation of the utilization ratio.For example: Unborrowed Amount = 500 Borrowed Amount = 500 Therefore util ratio = 50% Calculation in the LinearRateModel: idleAssetAmt = pool.totalAssets.assets = 1000 totalAssets = totalBorrows + idleAssetAmt = 500 + 1000 = 1500 util = totalBorrows / totalAssets = 500 / 1500 = 33.33%
Therefore the calculated utilization ratio is 33.33% when 50% of the funds are borrowed. As the utilization ratio is smaller than it should be the lender receives less interest than they should.
Recommendation
Do not calculate the
totalAssetsamount as theidleAssetAmtalready is thetotalAssetsamount.Resolution
Sentiment Team: The issue was resolved in PR#227.
-
H-10 High Impossible To Repay All Debt In Some Cases Rounding Resolved
Description
Proof of concept: PoC
Users can pay their Position’s whole debt by passing
type(uint256).maxvalue as the repayment amount. In that case, PositionManager contract will calculate Position’s total debt and make a call to the underlying pool for the payment.However, the
getBorrowsOffunction usesconvertToAssets, which rounds down by default, and this causes amount to be paid to round down. This amount is later used in therepayfunction.repayin the Pool contract also rounds down theborrowSharesamount to burn, which is done to ensure excess debt isn't pushed to other users.As a result of rounding down twice during the repay flow, the Position will always have 1
borrowShareleft even though the user tries to pay whole amount unless the amount is exactly the multiple ofasset:shareratio.This would cause repayment to fail due to
MIN_DEBTrequirement. Also, even if the Position has other debts that coverMIN_DEBT, the repaid debt pool can not be removed fromdebtPoolsarray from the Position contract due to remaining 1 share.Recommendation
Firstly,
getBorrowsOffunction should round up. Then, ensure that allborrowSharesare burned when all debt is paid.Resolution
Sentiment Team: The issue was resolved in PR#245.
-
H-11 High Funds Can Be Frozen By Reordering Queues Validation Resolved
Description
The SuperPool owner has the capability to reorder deposit and withdrawal queues. However, there is a lack of duplicate entry check in the
_reorderQueuefunction, which could potentially enable a malicious owner to freeze funds.For instance:
- withdrawQueue = [1, 2, 3]
- A malicious owner reorders with indices [0, 0, 0]
- Resulting withdrawQueue = [1, 1, 1]
As a result, the funds deposited in pools 2 and 3 become unwithdrawable.
Recommendation
It is recommended to implement a duplicate check within the function to prevent this issue from occurring. Verify the
indexesparam in the reorder queue functions contain all pool ids in the queue array.Resolution
Sentiment Team: The issue was resolved in PR#232.
-
H-12 High Max LTV Can Be Abused On Small Price Changes Validation Resolved
Description
Proof of concept: PoC
The given
maxLtvvalue provided by the protocol as a deployment parameter is 1e18 which equals a 1:1 ratio LTV. This means users can borrow assets for $100 by providing $100 collateral if the LTV for the given collateral token is set to 1e18. Therefore only a small price change not updated in the oracle because of deviation, or not yet updated as the attacker front runs the oracle update can be abused to make risk-free profit and put the pool into bad debt.Here is a possible attack scenario:
- The pool owner adds a new asset to the
BasePoolwith a 1e18 LTV (1:1 ratio) - The price of this asset is currently outdated as chainlinks deviation threshold is not reached (for
example 0.5% deviation and 0.4% change)
- The attacker takes a flash loan of the given asset and borrow all funds with it
- The attacker sells the funds from the
BasePoolfor a 0.4% - trading fees profit and pays back the
flashloan
- The attacker made a large profit depending on the size of the pool and the pool will be in bad debt
as soon as the oracle price is updated
Recommendation
Use a lower
maxLtvvalue.Resolution
Sentiment Team: The issue was resolved in PR#281.
- The pool owner adds a new asset to the
-
H-13 High DoS In _supplyToPools If One Deposit Fails DoS Resolved
Description
Every
depositcall could fail if theBasePoolis paused, or if the cap in theBasePoolis reached. As this call is not wrapped into a try-catch block, the transaction will revert and therefore the user is not able to deposit into the other pools of the queue. For example, if the firstBasePoolin the queue is paused the wholeSuperPooldeposit function is not usable.Recommendation
Wrap the
depositcall into a try-catch block as it is done with withdraws.Resolution
Sentiment Team: The issue was resolved in PR#234.
-
H-14 High DoS In _withdrawFromPools Based On Util Ratio DoS Resolved
Description
The
_withdrawFromPoolsfunction uses thegetAssetsOffunction of the givenBasePoolto calculate the maximum that can be withdrawn. But thegetAssetsOffunction returns the amount that theSuperPoolowns in theBasePool, not the amount that theSuperPoolcan withdraw right now.This can lead to reverts and using the queue in a suboptimal way:
- The
SuperPoolhas 100 tokens in theBasePool - The user tries to withdraw 15 tokens
- The util ratio of the
BasePoolis 95% (Only 10 tokens can be withdrawn right now as the rest is
borrowed)
- Therefore the
SuperPoolcould withdraw 10 tokens from this pool and 5 tokens from the next one - Instead, it tries to withdraw 15 tokens from this pool, the call fails and it will try to withdraw 15 from
the next one
- This reorders the queue in a suboptimal way
Recommendation
Reduce the borrowed funds from the
assetsInPool, or set theassetsInPoolto the balance of theBasePoolif it's smaller. Do not withdraw more than available liquidity.Resolution
Sentiment Team: The issue was resolved in PR#235.
- The
-
H-15 High Lenders can deposit into full SuperPools Validation Acknowledged
Description
The
_supplyToPoolsfunction loops through allBasePoolsin the queue and supplies funds to them as long as the cap is not reached. But this function does not revert if all caps were reached and it was impossible to supply funds to any pool. Therefore lenders are still able to deposit funds into a fullSuperPooland mint shares which is very capital inefficient and will reduce the yield per share of all lenders.Example:
- X lenders deposit a sum of Y USDC into pools with 5% APY over the
SuperPooland reach the cap - These lenders now receive 5% APY on their deposits
- More lenders deposit into the
SuperPoolwhich is already full - The new lender's funds are not put to work but they still receive shares of the
SuperPooland
therefore a share of the yield from the
BasePools- Every lender now receives less than 5% APY on their deposits
Recommendation
Revert at the end of the
_supplyToPoolsfunction.Resolution
Sentiment Team: Acknowledged.
- X lenders deposit a sum of Y USDC into pools with 5% APY over the
-
H-16 High Pool Initialized Multiple Times Logical Error Resolved
Description
Proof of concept: PoC
The
initializePoolfunction allows users to initialize a new pool, which users can then deposit the pool's assets into as well as access other functionalities. TheinitializePoolfunction enforces a check to ensure that an already initialized pool can’t be reinitialized, as this would reset the pool'stotalAssetsandtotalBorrowsback to zero, wiping out all previous users' deposits and debt.This check is done by ensuring that
ownerOf[poolId]is zero, which implies the newly created pool does not currently exist. The problem is that theinitializePoolfunction allows the owner address to be set to zero. Therefore, a pool could be created with theownerOf[poolId]as address zero, and since this check is used to ensure a pool is not being reinitialized, this will be bypassed in this case, and the pool can be reinitialized, resetting all values.Recommendation
Since the zero address is used as a check to ensure a pool ID does not already exist in the
initializePoolfunction, restrict users from being able to set the owner parameter as the zero address to avoid the problem above.Resolution
Sentiment Team: The issue was resolved in PR#238.
-
H-17 High Users Can Avoid Liquidations Logical Error Resolved
Description
Proof of concept: PoC
Users can borrow funds from pools using the
PositionManagercontract, as long as the position remains healthy, verified byriskEngine.isPositionHealthy(position).The
isPositionHealthyfunction has a flaw, as it has a condition where it can revert: if (totalDebtValue != 0 && totalDebtValue < MIN_DEBT) revert RiskModule_DebtTooLow(position,totalDebtValue);The
liquidatefunction usesisPositionHealthyto avoid liquidating healthy positions, but if the position is not healthy and thetotalDebtValueis less thanMIN_DEBT, the liquidation fails. AlthoughisPositionHealthyis checked when a position borrows assets, the eth value of the debt can decrease belowMIN_DEBT.Recommendation
Consider removing the
MIN_DEBTcheck fromRiskModuleand add it only to theborrowandrepayfunctions inPositionManagerResolution
Sentiment Team: The issue was resolved in PR#270.
-
H-18 High feeRecipient Set To Zero Blocks Functionality Validation Resolved
Description
The
feeRecipientis initially set during the deployment of a SuperPool and can also be changed using thesetFeeRecipientfunction in the SuperPool contract. This address receives fees when interest is accrued, which are minted as SuperPool shares to thefeeRecipient.The issue is that there is no restriction on the address to which the
feeRecipientcan be set. As a result, a SuperPool owner could inadvertently or maliciously set thefeeRecipientto the zero address. This will cause the accrue function, which is called in most operations, to revert, as the SuperPool_mintfunction will revert when the recipient address is zero.Recommendation
To prevent this issue, add a validation check in the
deploySuperPoolandsetFeeRecipientfunctions to ensure that thefeeRecipientaddress is not set to the zero address when the fee is greater than zero.Resolution
Sentiment Team: The issue was resolved in PR#246.
-
H-19 High Missing onlyFactory Check In SuperPool Validation Resolved
Description
Proof of concept: PoC
All pools exist within the singleton pool contract, with one singleton contract per SuperPool factory. Developer comments indicate that SuperPools should only be deployed through the factory contract to ensure they all point to the same singleton pool implementation.
However, the constructor of the SuperPool lacks a check to confirm it is being called by the factory. This opens the possibility for users to deploy a SuperPool that points to a malicious base pool. With multiple SuperPools containing the same asset, distinguishing between malicious SuperPools and regular ones becomes challenging for ordinary users.
Recommendation
Implement an
onlyFactorycheck to restrict SuperPool deployment to the factory only, preventing unauthorized users from deploying potentially harmful SuperPools.Resolution
Sentiment Team: The issue was resolved in PR#268.
-
M-01 Medium Unsafe Use Of transferFrom Validation Resolved
Description
Proof of concept: PoC
Some ERC-20 tokens return a boolean instead of reverting therefore using
transferFromwill not revert when the transfer fails.This can be abused in the liquidate function as the flow looks like the following:
- The liquidator repays the debt of the borrower:
- The funds are transferred from the liquidator to the position (if the transfer fails with such a token
nothing happens here)
- The debt of the borrower is reduced in the pool
- The liquidator seizes funds from the position as reward
Therefore if the
transferFromcall failed without reverting the debt of the borrower is reduced but the funds in the pool stayed the same and the liquidator receives rewards from the position for free.Recommendation
Use
safeTransferFrominstead oftransferFrom.Resolution
Sentiment Team: The issue was resolved in PR#219.
-
M-02 Medium Native Ether Can Not Be Deposited DoS Resolved
Description
The
execfunction of thePositioncontract can transfer native ether, as this might be needed to interact with some protocols (for example to pay gas for a 2-step flow).But it is not possible to use this feature as ether can not be deposited into the
Positioncontract:- The
depositfunction can not be used to deposit native ether - The
execfunction is not payable - And there is no receive or fallback function in the
Positioncontract
Recommendation
Implement the possibility to deposit native ether and/or make the
execfunctions payable and forward the msg.value from thePositionManagerto thePositioncontract.Resolution
Sentiment Team: The issue was resolved in PR#247.
- The
-
M-03 Medium superPoolCap Not Implemented Validation Resolved
Description
The
superPoolCapvariable is initialized and can be updated, but is never checked in the deposit flows (only in the maxDeposit view function).Recommendation
Check if the
superPoolCapis reached.Resolution
Sentiment Team: The issue was resolved in PR#228.
-
M-04 Medium Pausing The PositionManager Disables addToken DoS Resolved
Description
The
addTokenfunction is paused when thePositionManageris paused. This prevents borrowers from adding new tokens as collateral to their position, which could result in the borrowers not being able to keep their position healthy.Here is an example of such a scenario:
- A borrower opens a position with a collateral token (for example a BasePool or SuperPool share
token) and borrows funds
- Something bad happens in the system and the
PositionManageras well as the pool of the
collateral token is paused
- The collateral of the borrower losses value
- As the pool is paused the borrower is not able to get more tokens and increase the collateral of the
position
- Also as the
addTokenfunction is paused the borrower is not able to add a new collateral token to
the position The same could happen with another token that is for any reason not available at the given moment.
Recommendation
Remove the
whenNotPausedmodifier from theaddTokenfunction.Resolution
Sentiment Team: The issue was resolved in PR#267.
-
M-05 Medium Incorrect Key Is Used In PositionManager Validation Resolved
Description
Module addresses are fetched from the
Registrycontract and these addresses are stored with key to address mappings. Address keys are constant variables in contracts and they are determined using keccak hashes.SENTIMENT_POSITION_BEACON_KEYin the PositionManager contract is stated as “0xc77ea3242ed8f193508dbbe062eaeef25819b43b511cbe2fc5bd5de7e23b9990”. However, the correct hash result ofkeccak(SENTIMENT_POSITION_BEACON_KEY)is “0x6e7384c78b0e09fb848f35d00a7b14fc1ad10ae9b10117368146c0e09b6f2fa2”.If the
Registrycontract owner uses the correct key while setting addresses,positionBeaconaddress in the PositionManager contract will be retrieved incorrectly from the Registry contract.Recommendation
Use the correct hash for constant keys.
Resolution
Sentiment Team: The issue was resolved in PR#226.
-
M-06 Medium Repay & Seize Order Increases Bad Debt Risk Logical Error Resolved
Description
In the current
liquidateflow the liquidator has to first repay debt from the own pocket and receive funds from the liquidated borrower's position after that. If the unhealthy position is very big, fewer users (or no one) might be able to repay it from their own wallet.Reversing the order of this flow (seize before repay) could enable more users to have enough funds to repay the debt. This leads to more liquidations on time and therefore decreases the likelihood of bad debt.
Recommendation
Reverse the order of the repay and seize loop in the
liquidatefunction.Resolution
Sentiment Team: The issue was resolved in PR#248.
-
M-07 Medium Same Heartbeat Assumed For All Price Feeds Validation Resolved
Description
The same heartbeat (one hour) is assumed for all chainlink price feeds, but there are assets with different heartbeats for example 24 hours this will result in all operations with a 24-hour heartbeat asset to only work 1 hour a day and DoS the rest of the time.
Recommendation
Implement the possibility to set the heartbeat for each price feed individually.
Resolution
Sentiment Team: The issue was resolved in PR#239.
-
M-08 Medium Bad Debt Is Not Handled Logical Error Resolved
Description
At the moment the system does not handle bad debt, the liquidator always has to repay the full loan. If the loan to repay is higher than the assets in the
Positionit makes no economic sense to callliquidateas the liquidator would lose money. This leads to no one callingliquidateif bad debt occurs and the bad debt probably increases even further.The missing opportunity to take bad debt would lead to major problems in the system when a black swan event occurs.
Recommendation
Handle bad debt by repaying the maximum amount possible and either reducing the amount owned by the lenders or increasing the debt of the borrowers.
Resolution
Sentiment Team: The issue was resolved in PR#272.
-
M-09 Medium Preview Functions In SuperPool Are Not Accurate Logical Error Resolved
Description
SuperPool is a contract that is compatible with ERC4626. It includes multiple preview functions that, in the end, call internal functions
_convertToSharesor_convertToAssets.These internal functions utilize the
lastTotalAssetsvariable in their calculations. However,lastTotalAssetsdoes not represent the most updated asset amount, as it does not account for accrued interest since the last update. Therefore, the results of the preview functions are not accurate.Some more examples of non-compliance include:
previewDepositdoes not simulate accrue, sodepositmight mint less shares than previewed.previewMintdoes not simulate accrue, somintmight consume more assets than previewed.previewRedeemdoes not simulate accrue, soredeemmight withdraw less assets than previewed.previewWithdrawdoes not simulate accrue, sowithdrawmight burn more shares than previewed.maxDeposit/maxMintdo not correctly return the amount that can be deposited, as the cap can be
bypassed.
maxDeposit/maxMintdo not return 0 if pools are paused for depositsmaxWithdraw/maxRedeemreturn more assets than the real amount available, as
pool.getLiquidityOfadds interest accrued.depositshould revert if all ofassetscannot be deposited (due to poolCap limit)
Recommendation
To ensure accurate preview calculations, it is suggested to call the
simulateAccruefunction and utilize the returnednewTotalAssetsvalue. This will provide a more precise calculation of assets including accrued interest. Furthermore, consider making the Superpool EIP-4626 compliant.Resolution
Sentiment Team: The issue was resolved in PR#240.
-
M-10 Medium Fees Can Be Avoided With Dust Amounts Validation Resolved
Description
The origination fee (borrowing fee) rounds down (in favor of the borrower) and no minimum borrowing amount is enforced. Therefore borrowers can avoid paying borrowing fees by borrowing dust amounts multiple times.
For example: originationFee = 0.01e18 (1%) Amount to borrow = 99 fee = amt _ originationFee / 1e18 99 _ 0.01e18 = 0.99e18 fee = 0.99e18 / 1e18 = 0.99 = 0
This will likely lead to a loss (because of gas fees) for the borrower on most tokens (1e18 precision) but it could be profitable with low-precision tokens.
Recommendation
Implement a minimum borrow amount or round up (against the borrower).
Resolution
Sentiment Team: The issue was resolved in PR#269.
-
M-11 Medium Check If Asset Is Known In Deposit Flow Validation Resolved
Description
The PositionManager contract contains two mappings,
isKnownAddressandisKnownFunc, which define the universe of the protocol.isKnownAddressspecifies recognized addresses that a Position can interact with.The
transferfunction verifies whether the token is known as expected. However, thedepositfunction lacks this validation. Borrowers can deposit any asset to their position as collateral, causing these tokens to become locked since users are unable to transfer them afterwards. An analogous issue is present in theaddTokenfunction.Recommendation
Ensure that the address is verified in these functions.
Resolution
Sentiment Team: The issue was resolved in PR#250.
-
M-12 Medium Timelock Functionality Is Redundant Logical Error Resolved
Description
Some updates in the protocol require a 24-hour timelock period. These updates are requested initially and then accepted or rejected after the timelock period has elapsed.
However, the requester, accepter, and rejecter are all the same person. A malicious owner could request an update days or weeks before it is actually needed and simply wait for the opportune moment to accept it, rendering the timelock feature ineffective.
Recommendation
Consider implementing a deadline, such as 12 or 24 hours, for accepting a request after the timelock period has elapsed. Do not allow a pending request to be accepted after this deadline.
Resolution
Sentiment Team: The issue was resolved in PR#251.
-
M-13 Medium exec Should Have whenNotPaused Logical Error Resolved
Description
Some functions such as
borrow,addToken, andremoveTokenin the PositionManager contract have thewhenNotPausedmodifier. Theexecfunction should also have this modifier to prevent any unwanted actions from being executed when paused.Recommendation
Include the
whenNotPausedmodifier in theexecfunction. Additionally, reassess other functions liketransferto determine if they should be permitted when paused.Resolution
Sentiment Team: The issue was resolved in PR#267.
-
M-14 Medium Rebasing Tokens Are Not Supported Validation Resolved
Description
Proof of concept: PoC
The
BasePooluses internal variables to keep track of the pool balance as well as the user's balances and does not use the actual balance of the contract. Therefore rebalance and fee on transfer tokens will not work properly in the system.Here is an example of how a fee on transfer token would act in the system:
- User1 deposits 1000 tokens to the pool
- User2 deposits 1000 tokens to the pool
- User1 withdraws 1000 tokens from the pool
- User2 tries to withdraw 1000 tokens from the pool, but the call will revert as the pools balance is
lower than 1000 tokens because of the fee on every transfer
Recommendation
Update the system to support rebasing and fee on transfer tokens, or do not allow them by not setting oracles for these tokens and explicitly state these tokens are not supported.
Resolution
Sentiment Team: The issue was resolved in PR#265.
-
M-15 Medium setRegistry Should Invoke updateFromRegistry Logical Error Resolved
Description
The Pool and PositionManager contracts utilize the Registry contract to retrieve crucial module addresses, and both contracts feature an
updateFromRegistryfunction to update these addresses. In addition, both contracts are equipped with asetRegistryfunction that enables the owner to modify the Registry contract address.It is advisable to invoke the
updateFromRegistryfunction when setting a new Registry address in order to avoid potential mismatches. Failing to do so may result in the utilization of outdated module addresses until theupdateFromRegistryfunction is manually called.Recommendation
Always call the
updateFromRegistryfunction when implementing a new Registry address.Resolution
Sentiment Team: The issue was resolved in PR#252.
-
M-16 Medium Liquidator Can Seize Non-PositionAssets Logical Error Resolved
Description
Proof of concept: PoC
The liquidate function in the PositionManager contract allows anyone to liquidate an unhealthy position. Specifically, a user can select the amount and assets of debt to repay from the position and the amount and assets to seize from the position in return. The requirements being that the position must initially be unhealthy and end up being healthy after the liquidation, while also enforcing a maximum limit on the asset value that can be seized by the liquidator.
The issue is that, with the current implementation, the liquidator can seize assets not included in the position’s positionAssets list, as long as they are known assets. This should not be allowed, as the health check performed on the position only considers the assets in the position’s positionAssets list. Consequently, a user could unfairly lose assets that they did not intend to risk in a position by leaving them out of the asset list.
Recommendation
Modify the liquidate function to ensure that liquidators can only seize assets from a position's positionAssets list.
Resolution
Sentiment Team: The issue was resolved in PR#253.
-
M-17 Medium Interest Not Accrued Before Rate Update Logical Error Resolved
Description
The
acceptRateModelUpdatefunction allows the pool owner to change the pool's rate model to the pending one after the timelock duration (one day). This rate model is used in thesimulateAccruefunction to calculate the interest accrued in the pool for the duration since it was last called (pool.lastUpdated) until the current block.timestamp.The issue is that since the
acceptRateModelUpdatefunction doesn’t callaccruefirst, the next time a function that calls accrue is executed, the calculated interest will be based on the new rate model using the duration since thepool.lastUpdated, which could be a long time before the rate model was updated.Recommendation
The
acceptRateModelUpdatefunction should call accrue before updating the rate model to ensure that the interest calculation accurately reflects the old rate model up to the point of the update.Resolution
Sentiment Team: The issue was resolved in PR#223.
-
M-18 Medium Pool Cap Can Be Bypassed Reentrancy Resolved
Description
Proof of concept: PoC
Users can deposit the pool's respective asset by calling the deposit function in the pool contract. The function allows the pool owner to enforce a pool cap; however, when an ERC777 asset is used, this limit can be bypassed.
This issue arises because pool.totalAssets.assets is updated after the asset transfer, which is the point of reentrancy. A user can reenter with pool.totalAssets.assets not yet updated, so the limit check will use the old total asset value.
For example, if the totalAssets are 10 tokens away from the cap, a user could initially deposit 10 tokens. During the asset transfer, the user can reenter the function and make a second deposit of 10 tokens. As a result, totalAssets will end up being 10 tokens over the limit.
Recommendation
The asset transfer should be the initial step in the function to avoid a malicious state where tokens have not been transferred to the pool yet. Additionally, a nonReentrant modifier can be added to further secure the function.
Resolution
Sentiment Team: The issue was resolved in PR#236.
-
M-19 Medium Missing Pause Functionality Logical Error Resolved
Description
The contract
PositionManagerinherits fromPausableUpgradeablebut does not implement theonlyOwnerfunctions required to enable this functionality. As a result, the owner is unable to pause/unpause functions that have thewhenNotPausedmodifier.Additionally,
SuperPoolcontract inherits fromPausablecontract, does not use thewhenNotPausedor contains theonlyOwnerfunctions.Recommendation
To address this issue, it is recommended that
pauseandunpausefunctions be added to thePositionManagercontract.If
SuperPoolcontract is not meant to have pause functionality, consider removing the inheritance fromPausable.Resolution
Sentiment Team: The issue was resolved in PR#242.
-
M-20 Medium Chainlink Oracles Lack Proper Validation Oracle Resolved
Description
The
_getPriceWithSanityChecksfunction is used in the ChainlinkEthOracle and ChainlinkUsdOracle contracts to validate the fetched price updates from the Chainlink feed. However, in the current implementation, the validation only reverts when the price is less than zero, meaning a price of zero would be considered valid.This price is used to calculate position values and determine if positions can be liquidated. Consequently, positions could be incorrectly liquidated if a price of zero is returned instead of the oracle reverting.
Recommendation
Modify the price check to revert if the returned price is less than or equal to zero.
Resolution
Sentiment Team: The issue was resolved in PR#254.
-
M-21 Medium Reallocate Can Leave Assets In Contract Validation Acknowledged
Description
The
reallocatefunction will move funds from one pool to another one. However, there is no check that the total amount of assets redeemed as effectively deposited into the new pools. Assets not deposited will not earn interest, so SuperPool user's earnings will be affected.Recommendation
Verify that the total amount redeemed from pools matches the total amount deposited.
Resolution
Sentiment Team: Acknowledged.
-
M-22 Medium Missing Storage Gaps Logical Error Acknowledged
Description
Poolis an upgradeable contract that inherits from ERC6909 contract. However, this contract don’t use storage gaps, which will result in a corrupted storage if a variable is added/removed.Recommendation
Consider adding storage gaps in the ERC6909 contract.
Resolution
Sentiment Team: Acknowledged.
-
M-23 Medium Reallocate Will Redeem Assets Instead of Shares Logical Error Resolved
Description
Owner can reallocate funds by redeeming from certain pools and depositing into different pools. The issue arises when executing
POOL.redeemas it uses assets instead of shares. Not only owner will redeem more assets than expected, but assets might not be fully deposited during the second loop as total assets redeemed will be greater thatdepositstotal assets.Recommendation
The
withdrawsarray should contain shares instead of assets amount. Alternatively, calculate the shares that need to be redeemed with the asset amount passed and use that value inPOOL.redeem().Resolution
Sentiment Team: The issue was resolved in PR#222.
-
L-01 Low Accrue Before feeRecipient Update Logical Error Acknowledged
Description
If the owner of the
SuperPoollost access to thefeeRecipientwallet and tries to change it with thesetFeeRecipientfunction it first accrues interest. This would lead to further loss in this case.Recommendation
Update the
feeRecipientbefore calling theaccruefunction.Resolution
Sentiment Team: Acknowledged.
-
L-02 Low Allocators Can Bypass Pool Caps Validation Resolved
Description
The pool caps are not checked in the
reallocatefunction. Therefore allocators can bypass the pool caps set by the owner of theSuperPool.Recommendation
Check the pool caps in the
reallocatefunction.Resolution
Sentiment Team: The issue was resolved in PR#231.
-
L-03 Low approve Race Condition Logical Error Resolved
Description
The
ERC6909contract is vulnerable to a well-known race condition in the approve function:- User approves 100 tokens to a spender
- The user wants to increase the allowance to by 50 tokens and calls
approvewith 150 tokens - Spender front runs the call and spends 100 tokens
- The user's
approvecall goes through and the allowance is set to 150 tokens - Spender spends 150 tokens
Therefore the user wanted to allow the spender to spend 150 tokens but the spender was able to spend 250 tokens.
Recommendation
Implement increaseAllowance and decreaseAllowance functions.
Resolution
Sentiment Team: The issue was resolved in PR#256.
-
L-04 Low Missing Zero Assets Check In redeem Validation Resolved
Description
In the
redeemfunction of theSuperPoolcontract, it is not checked if the calculatedassetsamount frompreviewRedeemis 0. AspreviewRedeemrounds down it could be possible that a non-zero share amount is burned from the user but the user does not receive any assets in return.Recommendation
Revert if the calculated
assetsamount is 0.Resolution
Sentiment Team: The issue was resolved in PR#241.
-
L-05 Low Unused Events Superfluous Code Resolved
Description
The
PoolOwnerSetevent in the Pool contract and thePoolAddedevent in the SuperPool contract are not being emitted.Recommendation
It is recommended to either remove the unused events or to use them in appropriate functions.
Resolution
Sentiment Team: The issue was resolved in PR#257.
-
L-06 Low Typo Typo Resolved
Description
There are some typos in the codebase. PositionManager contract L30: “…liqudiator…” should be “…liquidator…” PositionManager contract L82: “SENIMENT” should be “SENTIMENT” RiskEngine contract L143: “…witihin…” should be “…within…”
Recommendation
We recommend updating the mentioned words in the codebase.
Resolution
Sentiment Team: The issue was resolved in PR#258.
-
L-07 Low Registry Address Can Be Immutable Optimization Resolved
Description
The
registryvariable in the RiskEngine contract is assigned in the constructor and there isn't any function for updating the 'registry' address in the contract.Recommendation
Consider making the variable immutable.
Resolution
Sentiment Team: The issue was resolved in PR#259.
-
L-08 Low Mismatch Between Code And Developer Comment Optimization Resolved
Description
In the borrow function of the Pool contract, the developer's comments state that minted shares should round up. However, the actual code rounds down because it uses
convertToShares.As rounding down is in favour of the borrower in this case. The excess debt is socialized among all other borrowers:
- Borrower borrowers X amount of funds
- The
convertToSharesfunction calculates that the given amount to borrow equals "Y + 0.9" debt
shares
- As solidity rounds down the user only gets Y debt shares
- Therefore the borrowed amount that equals 0.9 debt shares is socialized among all borrowers
Recommendation
Round up (against the borrower).
Resolution
Sentiment Team: The issue was resolved in PR#245.
-
L-09 Low Consider Using Ownable2Step Optimization Acknowledged
Description
The protocol utilizes the Ownable library from OpenZeppelin, which does not include a 2-step ownership change implementation. This could potentially lead to undesired situations such as contracts being left without an owner after an incorrect update.
Recommendation
It is advisable to consider utilizing the Ownable2Step library.
Resolution
Sentiment Team: Acknowledged.
-
L-10 Low Superfluous Code Superfluous Code Resolved
Description
In the
accruefunction of the Pool contract, the following lines are used twice:// Store a timestamp for this accrue() call // Used to compute the pending interest next time accrue() is called pool.lastUpdated = uint128(block.timestamp);Additionally, calling
OwnableUpgradeable.__Ownable_init()right before the internal_transferOwnership(owner_)in the Pool contract and PositionManager contract is unnecessary, as the latter call will override the initialization calls.Recommendation
Consider removing superfluous code to optimize the efficiency of the contracts.
Resolution
Sentiment Team: The issue was resolved in PR#260.
-
L-11 Low Can't Liquidate When Price Is Stale Oracle Acknowledged
Description
Liquidations use the health check to validate if the position can be liquidated. If the price is stale, health check reverts, preventing the liquidation. Protocol might want to liquidate positions, no matter if price is stale, to avoid bad debt.
Recommendation
If this is not the intended behavior, consider bypassing the stale price check only for the liquidation process
Resolution
Sentiment Team: Acknowledged.
-
L-12 Low Incorrect Pool Can Be Removed From Queue Logical Error Resolved
Description
The pool owner can remove a pool from the queue as long as there are no assets deposited. The
_removeFromQueuefunction contains a loop that searches for the index of the pool to remove, and then removes the first item, shifting all subsequent items down.While it is unlikely that the first loop will not find the poolId in the queue,
toRemoveIdxmay remain uninitialized, resulting in the removal of the first pool from the queue.Recommendation
Implement an early return inside the first loop if the index is not found.
Resolution
Sentiment Team: The issue was resolved in PR#261.
-
L-13 Low PositionManager Allows Free Flashloans Logical Error Acknowledged
Description
PositionManagerallows users to batch actions usingprocessBatch, with the condition that the position must be healthy when the transaction ends. This opens up the possibility of free Flash Loans, where users can borrow, transfer out the funds, use them outside of the protocol, and finally repay them to make sure the position is healthy.Recommendation
Although this will considerably increase the gas cost of the transaction, health can be checked in every action to ensure users can't borrow without depositing collateral first.
Resolution
Sentiment Team: Acknowledged.
-
L-14 Low Superfluous OnlyOwner Modifier Logical Error Resolved
Description
_removePoolhas anonlyOwnermodifier, although this is an internal function that can only be called from thesetPoolCapwhich already has the access control.Recommendation
Remove the
onlyOwnermodifier from the_removePoolfunction.Resolution
Sentiment Team: The issue was resolved in PR#261.
-
L-15 Low Liquidation Discount Is Incorrect Math Resolved
Description
Liquidators can get borrowers collateral at a discounted price. According to deployment script it will be 20%. Maximum seized collateral amount is calculated with this formula:
debtRepaidValue.mulDiv((1e18 + LIQUIDATION_DISCOUNT), 1e18)However, the correct way to implement this discount should be
debtRepaidValue.mulDiv(1e18, (1e18- LIQUIDATION_DISCOUNT))For example, getting $100 worth of asset with 20% discount means that
the user should pay 80$. But in the current implementation, the user gets $120 worth of asset by paying $100, effectively making the discount 16.66%.
Recommendation
Consider implementing the discount by decreasing the denominator instead of increasing the numerator. Otherwise, document this behavior as accepted.
Resolution
Sentiment Team: The issue was resolved in PR#262.
No findings match.
Invariants 108
The review's fuzzing suite asserted 108 invariants. 99 held and 9 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
SP-01 | SuperPool.deposit() must consume exactly the number of assets requested | Held |
SP-02 | SuperPool.deposit() must credit the correct number of shares to the receiver | Held |
SP-03 | SuperPool.deposit() must credit the correct number of assets to the pools in | Held |
SP-04 | depositQueue SuperPool.deposit() must credit the correct number of shares to the pools in | Held |
SP-05 | depositQueue SuperPool.deposit() must credit the correct number of shares to the SuperPool for the | Held |
SP-06 | pools in depositQueue SuperPool.deposit() must update lastUpdated to the current block.timestamp | Held |
SP-07 | for the pools in depositQueue SuperPool.deposit() must credit pendingInterest to the totalBorrows asset | Held |
SP-08 | balance for pools in depositQueue SuperPool.deposit() must transfer the correct number of assets to the base pool | Held |
SP-09 | for pools in depositQueue SuperPool.deposit() must increase the lastTotalAssets by the number of assets provided | Held |
SP-10 | SuperPool.deposit() must always mint greater than or equal to the shares | Held |
SP-11 | predicted by previewDeposit() SuperPool.mint() must consume exactly the number of tokens requested | Held |
SP-12 | SuperPool.mint() must credit the correct number of shares to the receiver | Held |
SP-13 | SuperPool.mint() must credit the correct number of assets to the pools in | Held |
SP-14 | depositQueue SuperPool.mint() must credit the correct number of shares to the pools in | Held |
SP-15 | depositQueue SuperPool.mint() must credit the correct number of shares to the SuperPool for the | Held |
SP-16 | pools in depositQueue SuperPool.mint() must update lastUpdated to the current block.timestamp for the | Held |
SP-17 | pools in depositQueue SuperPool.mint() must credit pendingInterest to the totalBorrows asset | Held |
SP-18 | balance for pools in depositQueue SuperPool.mint() must transfer the correct number of assets to the base pool for | Held |
SP-19 | pools in depositQueue SuperPool.mint() must increase the lastTotalAssets by the number of assets consumed | Held |
SP-20 | SuperPool.mint() must always consume less than or equal to the tokens predicted | Held |
SP-21 | by previewMint() SuperPool.withdraw() must credit the correct number of assets to the receiver | Held |
SP-22 | SuperPool.withdraw() must deduct the correct number of shares from the owner | Held |
SP-23 | SuperPool.withdraw() must withdraw the correct number of assets from the pools in | Broken |
SP-24 | withdrawQueue SuperPool.withdraw() must withdraw the correct number of shares from the pools in | Broken |
SP-25 | withdrawQueue SuperPool.withdraw() must deduct the correct number of shares from the SuperPool share balance for the pools in | Broken |
SP-26 | withdrawQueue SuperPool.withdraw() must update lastUpdated to the current block.timestamp for the pools in | Held |
SP-27 | withdrawQueue SuperPool.withdraw() must credit pendingInterest to the totalBorrows asset | Held |
SP-28 | balance for pools in withdrawQueue SuperPool.withdraw() must transfer the correct number of assets from the base pool for pools in withdrawQueue | Broken |
SP-29 | SuperPool.withdraw() must decrease the lastTotalAssets by the number of assets | Held |
SP-30 | consumed SuperPool.withdraw() must redeem less than or equal to the number of shares | Held |
SP-31 | predicted by previewWithdraw() SuperPool.redeem() must credit the correct number of assets to the receiver | Held |
SP-32 | SuperPool.redeem() must deduct the correct number of shares from the owner | Held |
SP-33 | SuperPool.redeem() must withdraw the correct number of assets to the pools in | Broken |
SP-34 | withdrawQueue SuperPool.redeem() must withdraw the correct number of shares from the pools in | Broken |
SP-35 | withdrawQueue SuperPool.redeem() must deduct the correct number of shares from the SuperPool share balance for the pools in | Broken |
SP-36 | withdrawQueue SuperPool.redeem() must update lastUpdated to the current block.timestamp for the pools in | Held |
SP-37 | withdrawQueue SuperPool.redeem() must credit pendingInterest to the totalBorrows asset balance for pools in withdrawQueue | Held |
SP-38 | SuperPool.redeem() must transfer the correct number of assets from the pools in | Broken |
SP-39 | withdrawQueue SuperPool.redeem() must decrease the lastTotalAssets by the number of assets | Held |
SP-40 | consumed SuperPool.redeem() must withdraw greater than or equal to the number of assets | Held |
SP-41 | predicted by previewRedeem() The lastTotalAssets value before calling accrue should always be <= after calling it | Broken |
SP-42 | Fee recipient shares after should be greater than or equal to fee recipient | Held |
SP-43 | shares before previewDeposit() must not mint shares at no cost | Held |
SP-44 | previewMint() must never mint shares at no cost | Held |
SP-45 | convertToShares() must not allow shares to be minted at no cost | Held |
SP-46 | previewRedeem() must not allow assets to be withdrawn at no cost | Held |
SP-47 | previewWithdraw() must not allow assets to be withdrawn at no cost | Held |
SP-48 | convertToAssets() must not allow assets to be withdrawn at no cost | Held |
SP-49 | Profit must not be extractable from a convertTo round trip (deposit, then | Held |
SP-50 | withdraw) Profit must not be extractable from a convertTo round trip (withdraw, then | Held |
SP-51 | deposit) Shares must not be minted for free using deposit() | Held |
SP-52 | Shares must not be minted for free using mint() | Held |
SP-53 | Assets must not be withdrawn for free using withdraw() | Held |
SP-54 | Assets must not be withdrawn for free using redeem() | Held |
SP-55 | The vault's share token should have greater than or equal to the number of decimals as | Held |
SP-56 | the vault's asset token Share inflation attack possible, victim lost an amount over lossThreshold% | Held |
PO-01 | Pool.deposit() must increase poolId assets by assets and pending interest | Held |
PO-02 | Pool.deposit() must increase poolId shares by sharesDeposited | Held |
PO-03 | Pool.deposit() must consume the correct number of assets | Held |
PO-04 | Pool.deposit() must credit the correct number of shares to receiver | Held |
PO-05 | Pool.deposit() must transfer the correct number of assets to pool | Held |
PO-06 | Pool.deposit() must update lastUpdated to the current block.timestamp | Held |
PO-07 | Pool.deposit() must credit pendingInterest to the totalBorrows asset balance for | Held |
PO-08 | poolID Pool.redeem() must decrease poolId assets by assetsRedeemed + | Held |
PO-09 | pendingInterest Pool.redeem() must decrease poolId shares by shares amount | Held |
PO-10 | Pool.redeem() must credit the correct number of assets to receiver | Held |
PO-11 | Pool.redeem() must consume the correct number of shares from receiver | Held |
PO-12 | Pool.redeem() must transfer the correct number of assets to receiver | Held |
PO-13 | Pool.redeem() must update lastUpdated to the current block.timestamp | Held |
PO-14 | Pool.redeem() must credit pendingInterest to the totalBorrows | Held |
PO-15 | asset balance for poolID The pool.totalAssets.assets value before calling accrue should always be | Held |
PO-16 | <= after calling it. The pool.totalBorrows.assets value before calling accrue should always be | Held |
PO-17 | <= after calling it Fee recipient shares after should be greater than or equal to fee recipient | Held |
PO-18 | shares before User debt value should be equal to 0 or greater than or equal to MIN_DEBT | Held |
PO-19 | Min Required Position Asset Value should be greater than total position | Held |
PO-20 | debt value The pool.totalAssets.shares values should always equal the sum of the | Held |
PO-21 | shares of all users The pool.totalBorrows.shares values should always equal the sum of the borrow share balances of all borrowers | Held |
PM-01 | PositionManager.newPosition() should set auth to true for owner | Held |
PM-02 | PositionManager.newPosition() should set ownerOf position to owner | Held |
PM-03 | PositionManager.deposit() must consume the correct amount of assets | Held |
PM-04 | PositionManager.transfer() must consume asset amount from position | Held |
PM-05 | PositionManager.transfer() must credit asset amount to recipient | Held |
PM-06 | PositionManager.borrow() must credit amount of assets to poolId total borrow | Held |
PM-07 | asset balance PositionManager.borrow() must credit amount of shares to poolId total borrow | Held |
PM-08 | share balance PositionManager.borrow() must credit amount of shares to poolId position share | Held |
PM-09 | balance PositionManager.borrow() must credit fee amount to feeRecipient | Held |
PM-10 | PositionManager.borrow() must credit the correct number of assets to position | Held |
PM-11 | PositionManager.borrow() must add poolId to debtPools | Held |
PM-12 | Position debt pools should be less than or equal to max debt pools | Held |
PM-13 | PositionManager.repay() must credit assets to pool | Held |
PM-14 | PositionManager.repay() must consume asset amount from position | Held |
PM-15 | PositionManager.repay() must consume amount of assets from poolId total borrow | Held |
PM-16 | asset balance PositionManager.repay() must consume amount of shares from poolId total borrow | Held |
PM-17 | share balance PositionManager.repay() must consume amount of shares from poolId position | Held |
PM-18 | share balance PositionManager.repay() must delete poolId from debtPools if position has no | Held |
PM-19 | borrows PositionManager.addToken() must add asset to position assets list | Held |
PM-20 | Position assets length should be less than or equal to max assets | Held |
PM-21 | PositionManager.removeToken() must remove asset from position assets list | Held |
PM-22 | PositionManager.liquidate() must credit the correct number of debt assets to pool | Held |
PM-23 | PositionManager.liquidate() must credit the correct number of debt assets to poolPositionManager.liquidate() must | Held |
PM-24 | credit the correct number of assets to liquidator PositionManager.liquidate() must credit the correct number of fee assets to owner | Held |
PM-25 | PositionManager.liquidate() must consume the correct number of position assets from | Held |
PM-26 | position Position must be healthy after liquidation | Held |
PM-27 | PositionManager.liquidate() must consume the correct number of assets from the | Held |
PM-28 | pools in debtData PositionManager.liquidate() must consume the correct number of shares from the | Held |
PM-29 | pools in debtData PositionManager.liquidate() must consume amount of shares from poolId position | Held |
PM-30 | share balance PositionManager.liquidate() must update lastUpdated to the current | Held |
PM-31 | block.timestamp for the pools in debtData PositionManager.liquidate() must delete poolId from debtPools if position has no borrows | 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.
