Crit/HighLost withdrawals due to ManagedLeveragedVault::openDen() using all asset() balanceH-1
ManagedLeveragedVault::openDen() picks up all asset() and converts to collateral. However, some of these assets may be reserved for withdrawals, in which case the tracking would be completely off.
MediumManagedLeveragedVault.sol::deposit() is missing slippage controlM-1
ManagedLeveragedVault.sol::deposit() doesn't have slippage control for the shares minted.
MediumDoSed withdrawals due to ManagedLeveragedVault::executeWithdrawalEpoch() repaying debt above limitM-2
ManagedLeveragedVault::executeWithdrawalEpoch() repays debt to withdraw collateral from the Den to send to users, but it doesn't handle when the minimum debt limit is reached and becomes DoSed.
MediumManagedLeveragedVault::executeWithdrawalEpoch() will never work because cd.prevICR is not setM-3
ManagedLeveragedVault::executeWithdrawalEpoch() asserts the new ICR is close to the old one, but the latter is never set. Marking medium as the contract is upgradeable.
MediumManagedLeveragedVault::increaseLeverage() fails when the debt is closed to the limitM-4
ManagedLeveragedVault::increaseLeverage() will revert when the protocol is close to the max debt as it needs to borrow more than the debt allows in order for the ICR to reach the target.
MediumManagedLeveragedVault::decreaseLeverage() will not work when it goes below the minimum debtM-5
ManagedLeveragedVault::decreaseLeverage() is used to increase the ICR of the Den, but it may be DoSed in case it goes below the minimum debt limit.
MediumProtocol will need to donate minimum debt for all new ManagedLeveragedVaultsM-6
ManagedLeveragedVault::deposit() doesn't allow depositing before the Den is opened, which means the only way to have a minimum collateral amount to open the minimum debt in ManagedLeveragedVault::openDen() is by…
MediumMissing several safeERC20 functionsM-7
Some tokens will not work currently due to not using safeERC20. approve(), transferFrom() don't work for all tokens and safeERC20 should be used. Protocol can't be used for certain tokens.
MediumManagedLeveragedVault::executeWithdrawalEpoch() incorrect ICR and DoSed withdrawals due to not accouting for feesM-8
ManagedLeveragedVault::executeWithdrawalEpoch() applies the exposure - nect swap fees loss on the users withdrawing, ending up repaying more debt than the collateral withdrawn, increasing the ICR and possibly reverting…
Low/InfoUnused/useless codeL-1
There are multiple unused pieces of code. See links below. Deployment costs Manual Review Remove this code.
Low/InfoHints on withdrawal may fail as the ICR changes between claiming collateral surplus and repaying debtL-10
Insertion hints depend on the ICR of the Den, on withdrawals the same hints are used in 2 different instances, which may not work and revert.
Low/InfoManagedLeveragedVault::deposit() slippage control on collVaultShares is not intuitiveL-2
ManagedLeveragedVault::deposit() has slippage control for collVaultShares but it applies to the full deposited collateral when the user only gets a share of it due to entry fees.
Low/InfoIncorrect ERC4626ExceededMaxRedeem event on ManagedLeveragedVault.sol:: cancelWithdrawalIntent()L-3
ERC4626ExceededMaxRedeem first argument is owner, but the code emits it with receiver. Full function: It shows that ERC4626ExceededMaxRedeem is emitted with msg.sender, which is the receiver, not the owner.
Low/InfoManagedLeveragedVault::increaseLeverage() will never work in Recovery modeL-4
Recovery mode doesn't allow Dens to decrease their ICR. Hence, ManagedLeveragedVault::increaseLeverage() will revert when the protocol is in Recovery mode.
Low/InfoManagedLeveragedVault::getAvailableDebt() used in ManagedLeveragedVault::increaseLeverage() is incorrectL-5
ManagedLeveragedVault::increaseLeverage() allows borrowing until the maxSystemDebt limit, but the check doesn't account for the borrowing fee triggered.
Low/InfoManagedLeveragedVault::donateCollateral() is missing a slippage checkL-6
ManagedLeveragedVault::donateCollateral() doesn't control slippage, leading to losses The function is: As can be seen, there is no slippage check in the coll vault, possibly leading to fund loss.
Low/InfoSome view functions will not work under certain conditionsL-7
ManagedLeveragedVault::getStrategyCR(), ManagedLeveragedVault::exposurePositionWeight() and ManagedLeveragedVault:: getStrategyLeverage() may revert.
Low/InfoManagedLeveragedVault::getDebtToUnwindAndCollRequested() is inaccurate when there are surplus tokensL-8
ManagedLeveragedVault::getDebtToUnwindAndCollRequested() is called on chain after the modifier claimCollateralSurplus is called, so it is updated, but offchain this modifier was not called and the view function is off.
Low/InfoMost ManagedLeveragedVault functions are DoSed due to bad debt checkL-9
Most ManagedLeveragedVault functions rely on totalAssets(), including ERC4626 compliant functions that must not revert; however, they will revert when there is bad debt.
Crit/HighStrategy main ticks are set according to the tick in slot0, leading to incorrect allocation and loss of fundsH-2
Main position ticks are set according to the tick in slot0, which is not accurate if the price is near a border.
Crit/HighBase calculation in Leverager::isLiquidateable() is incorrect as the max leverage may be smallerH-4
Base calculation in Leverager::isLiquidateable() is: uint256 base = owedAmount 1e18 / (vp.maxTimesLeverage - 1e18);.
Crit/HighContradiction between high-leverage and liquidation check of positionH-5
Market can allow leverage higher than 2x, and this is design choice. But the check owedAmount totalDenom in isLiquidateable function doesn't allow leverage higher than 2x.
MediumVault::_calcDeposit() will overflow for low priced tokensM-1
Vault::calcDeposit() calculates the shares as depositAmount bal totalSupply in the numerator. If each of these quantities has 18 decimals, this is 1e54 of precision, having only around 1e23 left.
MediumLeverager::deposit, does not support multi-hop swaps with exactOutputM-10
When depositing into the leverager, if the borrowed token is not token0 or token1, the function has to perform an exactOutput swap, to receive the borrowed token0/token1.
MediumReserveLogic::_updateIndexes() assumes the utilization rate was constant the whole time when calculating the new borrowsM-2
ReserveLogic::updateIndexes() is as follows: Note that when calculating the newBorrowingIndex, it calls latestBorrowingIndex(reserve), which uses reserve.currentBorrowingRate, which was calculated at the last time the…
MediumStrategy main ticks are not symmetric when the tick spacing is one due to incorrect isLowerSided inequalityM-3
Strategy main position ticks are set according to: As can be seen, when the tick spacing is 1, the tick will be deemed lower sided, which adds a tick spacing (1) to the tick border.
MediumStrategy::checkPoolActivity() incorrect check leads to vulnerable priceM-4
Strategy::checkPoolActivity() reverts if the timestamp is 0. However, this will never happen.
MediumStrategy::checkPoolActivity() does not look as far back as it shouldM-5
Strategy::checkPoolActivity() is supposed to check tick delta until the tick more in the past than lookAgo.
MediumIncorrect modulo calculation in secondary position ticks leads to active position and division by zeroM-6
The StrategysetSecondaryPositionsTicks function incorrectly handles negative ticks when calculating modulo, causing the secondary position to be active when it should be out-of-range and potentially leading to division…
MediumVault::withdraw() withdraws too much liquidity leading to idle capital and loss of feesM-7
Vault::withdraw() withdraws from the lp position whenever the idle capital is not enough. However, it withdraws too much as it does not take into account that some of the idle capital is already available.
MediumLocked funds due to underflow in withdrawalM-9
In the withdraw function of the Leverager contract, when the borrowed asset is token1, the code uses the wrong variable in its conditional subtraction.
Crit/HighBorrower withdrawing at a loss will cause losses for cds depositors that only withdraw after the price recoversH-11
Borrowers withdrawing at a loss get downside protection from cds depositors. If these cds depositors withdraw at the same price as the borrower withdrawal that triggered the loss, it works correctly.
Crit/HighTotal cds deposited amount is incorrectly modified when cds depositor is at a loss, leading to stuck USDaH-12
The total cds deposited amount is decreased by the returned amount when the cds depositor withdraws, which will include any loss that the cds depositor has taken.
Crit/HighCds depositors profit up to the strike price is not redeemable as the total cds deposited amount is not increasedH-13
Cds depositors get the profit on the eth price increase up to the strike price (5%), and any increase above this threshold goes to the borrowers.
Crit/HighCds depositor profit is never taxed as the tax is only applied on the option feesH-14
Cds depositor generates upside from borrows up to the strike price increase, but calculates the profit on the difference between the deposited and returned amounts, which only differ by the option fees.
Crit/HighType 1 borrower liquidation will incorrectly add cds profit directly to totalCdsDepositedAmountH-15
Borrower liquidations of type 1 add a cds profit component to cds depositors by reducing totalCdsDepositedAmount by a smaller amount of this profit.
Crit/HighLiquidation profit is never given to cds depositors who will take these lossesH-16
Liquidation profit is calculated in borrowingLiquidation::liquidationType1(), but is actually never handled and given to cds depositors.
Crit/Highborrowing::withdraw() at a loss will increase downside protected and misscalculate option feesH-17
borrowing::withdraw() at a loss increases the downside protected, here.
Crit/HighSome liquidated collateral will be lockedH-18
After we liquidate via liquidation type1 method, all cds owner's liquidation share will change. This will cause some liquidated collateral will be locked.
Crit/HighCds amounts to reduce from each chain are incorrect and will lead to the inability to withdraw cds in one of the chainsH-19
The cds amount to reduce on liquidations from each chain is given by the share of cds deposit, but then the liquidation amount by each cds depositor is given by the available liquidation amount pro-rata to the…
Crit/HighPotential Underflow in withdrawInterestH-2
In the withdrawInterest function, if totalInterest is less than amount, subtracting amount from totalInterest will cause an underflow and revert the transaction due to Solidity 0.8.x's checked arithmetic, even if…
Crit/Highborrowing::liquidate() sends the wrong liquidation index to the destination chain, overwritting liquidation information and getting collateral stuckH-20
borrowing::liquidate() sends the noOfLiquidations variable as liquidation index to the other chain. However, liquidations are tracked in omniChainData.noOfLiquidations, on both chains.
Crit/HighMalicious user can call borrowing::calculateCumulativeRate() any number of times to inflate debt rate as lastEventTime is not updatedH-21
borrowing::calculateCumulativeRate() does not update lastEventTime, so the cumulative rate may be increased unbounded, forcing users to repay much more debt.
Crit/HighLate abond holders steal USDa amount from liquidations from earlier abond holdersH-22
BorrowLib::redeemYields() gets usdaToAbondRatioLiq, which is the amount of USDa gained from liquidations pro-rata to the total supply of abond, at current value without consideration from previous depositors.
Crit/HighCDSLib::withdrawUserWhoNotOptedForLiq() tax is not stored in the treasuryH-23
CDSLib::withdrawUserWhoNotOptedForLiq() does not store the tax from the cds depositor profit, leaving these funds stuck.
Crit/HighUsing LayerZero for synchronizing global states between two chains may lead to overwriting of global states.H-26
Due to the lack of a locking mechanism and the non-atomic nature of cross-chain operations, global states may be at risk of being overwritten.
Crit/HighBorrower deposit, withdraw, deposit will reinit omniChainData.cdsPoolValue, getting profit stuck for cds depositorsH-27
BorrowLib::calculateRatio() sets omniChainData.cdsPoolValue to previousData.totalCDSPool + netPLCdsPool, disregarding any past value of omniChainData.cdsPoolValue, when the noOfDeposits is null.
Crit/HighMissing Update to omnichain.totalAvailableLiquidationAmount in withdrawUserH-28
In the deposit function we update omnichain.totalAvailableLiquidationAmount based on the enw deposit, however The Withdraw function processes user withdrawals but fails to update the…
Crit/HighLiquidation will reduce total cds deposited amount, leading to incorrect option feesH-29
borrowLiquidation::liquidationType1() reduces omniChainData.totalCdsDepositedAmount, so the option fees will be calculated on this reduced total cds depositors, but the normalized deposit of each borrower still amounts…
Crit/HighBorrowing::redeemYields debits ABOND from msg.sender but redeems to user using ABOND.State data from userH-3
Borrowing::redeemYields debits ABOND from msg.sender (which updates its state), but uses the information of the user passed to calculate the amount to be withdrawn from the external protocol to send to the user.
Crit/HighThe user overpays the USDA amount for downside protection while withdrawingH-32
The protocol provides = 20% downside protection on the collateral depending on the volatility of collateral.
Crit/HightotalCdsDepositedAmountWithOptionFees is incorrectly reduced in CDSLib::withdrawUser(), leading to stuck option feesH-33
totalCdsDepositedAmountWithOptionFees in CDSLib::withdrawUser() is reduced by: This is incorrect as the correct amount to reduce in option fees is params.optionFees - params.optionsFeesToGetFromOtherChain, not…
Crit/HighStrike Price Not Validated Against Strike Percent, Leading to Exploitation RiskH-36
The strikePrice and strikePercent are important parameters in the Borrowing::depositTokens function, which are passed to the BorrowLib::deposit function for option fee calculations.
Crit/Hightreasury.updateYieldsFromLiquidatedLrts() updates the yield in the current chain, but collateral may be in the other chainH-37
treasury.updateYieldsFromLiquidatedLrts() updates the yield from liquidated collateral in the current chain, but this collateral could have been present in the other chain.
Crit/HighodosAssembledData can be manipulatedH-4
The signature in withDraw can be reused and this will cause users can choose one improper odosAssembledData and convert less collateral than expected. In borrowing.sol:withDraw, borrowers can withdraw their collateral.
Crit/Highcds owners can withdraw more than expected via manipulating excessProfitCumulativeValueH-5
excessProfitCumulativeValue in withdraw() can be manipulated. Malicious users can manipulate this excessProfitCumulativeValue to withdraw more than expected.
Crit/HighMalicious users can DOS the protocol by setting downsideProtected to a large valueH-8
In CDS.sol updateDownsideProtected() has no access control so malicious users can set downsideProtected to a large value which will DOS the system.
MediumAccumulated profit/losses by the cumulative value is not dealt with in borrowingLiquidation::liquidationType1(), leading to lossesM-19
borrowingLiquidation::liquidationType1() reduces omniChainData.totalVolumeOfBorrowersAmountinWei and omniChainData.totalCdsDepositedAmount without any consideration for the cumulative values so far, leading to losses.
MediumInterest generated by last bond will not go to anyone when liquidating as there is no bond amount to collect itM-21
borrowingLiquidation::liquidationType1() withdraws the bond from the external protocol and updates the interest, but does not check if there is bond supply to distribute the interest to.
MediumGlobalVariables::oftOrCollateralReceiveFromOtherChains() calculates the fee as if it was the same in both chains, which is falseM-23
GlobalVariables::oftOrCollateralReceiveFromOtherChains() send a message to the other chain requesting token/eth transfers to the current chain.
MediumGlobalVariables::oftOrCollateralReceiveFromOtherChains() always charges twice the collateral on COLLATERAL_TRANSFER, which is not neededM-24
GlobalVariables::oftOrCollateralReceiveFromOtherChains() frequently occurs losses by forwarding too much ETH as it assumes collateral transfer always happen for the 2 tokens + eth, which is not true as it can just be…
MediumYield form LRTs are forever stuck in the protocol and cannot be withdrawnM-26
During borrower liquidation (liquidationType1()) and withdrawal from the CDS, the yield from LRTs is updated and stored in the treasury.
MediumDOS on liquidation type 1 due to underflow in cds profits computationM-27
An underflow in cds profits computation can cause liquidation type 1 to revert. Here is the formula for cds profits computation.
MediumProtected downside is not updated when cds.getTotalCdsDepositedAmount() < downsideProtectedM-29
The borrowing::withdraw is used to withdraw collateral from the protocol and burn USDa.
MediumTreasury.noOfBorrowers can be set to 0 by looping wei deposit<->withdrawals and DoS withdrawals and reset borrower debtM-30
Treasury.noOfBorrowers is increased when a user deposits for the first time, as the index is 0.
MediumCDSLib::calculateCumulativeRate() incorrectly only increment the local option fees when there are cds depositsM-32
CDSLib::calculateCumulativeRate() adds option fees to totalCdsDepositedAmountWithOptionFees if totalCdsDepositedAmount is not null, skipping if it is null.
MediumInconsistent Use of lastCumulativeRate in depositTokens() and withdraw() Functions in Borrowings ContractM-33
The depositTokens() and withdraw() functions in the Borrowing contract exhibit inconsistent use of the lastCumulativeRate.
MediumWithdrawing ionic during liquidation has a flawM-5
During liquidation only record yield accrued from IONIC, however those yields are not utilized/there is no way to withdraw them.
MediumAn attacker can manipulate omniChainData.cdsPoolValue by breaking protocol.M-6
Missing update of lastEthprice in borrowing.soldepositTokens() will cause manipulation of omniChainData.cdsPoolValue as an attaker replays borrowing.soldepositTokens() by breaking protocol.
Crit/HighFunds may be stolen by calling onMoreFlashLoan() directly3S-MORE-C01
LoopStrategy::onMoreFlashLoan() does not validate that the caller is the market, which allows attackers to steal collateral by calling it directly. Revert if the caller is not markets. Addressed in 7d71809.
Crit/HighMalicious path can be passed to redeem/withdraw() allowing an attacker to draing the strategy3S-MORE-C02
path is not validated in redeem/withdraw(), which could lead to problems as the flow should always sell and buy the same token.
Crit/HighMarket interest is not always accrued3S-MORE-H01
The market accrues interest, which changes totalAssets() in theLoopStrategy. Thus, everytime there is some action that modifies the amount of assets, interest should always be accrued first.
MediumVault cap is not considered in the maxDeposit() calculations, which may make deposits fail3S-MORE-M01
LoopStrategy::maxDeposit() does not consider the vault cap, which limits the amount that can be deposited if the utilization rate is close to the targetUtilization. Hence, deposits will revert.
MediumLoopStrategy is vulnerable to inflation attacks3S-MORE-M02
LoopStrategy inherits ERC4626Upgradeable, which calculates shares deposited as As such, the following attack is possible: - User deposits 1e18 assets.
Low/InfoIn case of high ltv and unfavourable swap, onMoreFlashLoan() may underflow3S-MORE-L01
LoopStrategy::onMoreFlashLoan() underflows when cost collateralToWithdraw, which may happen in case the market has a high ltv and significant slippage occurs. Consider using the wFlow from the vault in this case.
Low/InfoLoopStrategy::totalAssets() tracks the wFlow balance, but this is not redeemable3S-MORE-L02
The LoopStrategy is not supposed to hold wFlow directly, but this is still tracked in totalAssets(). However, this is not redeemed in withdraw(), so it will be stuck.
Low/InfoIn LoopStrategy::withdraw(), _updateLastTotalAssets() is called after _withdraw()3S-MORE-N01
In LoopStrategy::withdraw(), updateLastTotalAssets() is called after withdraw(). Follow the CEI pattern and update the total assets before the withdraw() call. Addressed in c4b4f1a.
Low/Info> should be used instead of!= in LoopStrategy::_deposit()3S-MORE-N02
LoopStrategy::deposit() stops iterating if the utilization ratio reaches exactly the target, that is, market.totalSupplyAssets.wMulDown(targetUtilization) == totalBorrowAssets.
Low/InfoTypo in LoopStrategy::_maxDeposit()3S-MORE-N03
LoopStrategy::maxDeposit() has as a comment totolSuppliable, which has a typo. Replace totolSuppliable with totalSuppliable. Addressed in 40132e8.
Low/InfoApproval is not reset to 0 after approving the markets for wFlow assets repayment in LoopStrategy::onMoreFlashLoan()3S-MORE-N04
LoopStrategy::onMoreFlashLoan() does not reset the approval of the markets to 0 after approving the repayment of wFlow.
Low/InfosafeApprove() should be used instead of approve()3S-MORE-N05
Some tokens do not return a bool on approval, so safeApprove() should be used. Consider replacing all instances of approve() by safeApprove(). Can be kept as is as the current tokens work without issues.
Low/InfoparamsOfTheMarket is unused in the LoopStrategy3S-MORE-N06
paramsOfTheMarket is unused in the LoopStrategy. Remove or deprecate the variable (comment or rename it). Addressed in 50a9248.
Low/InfoMapleSkyStrategy:: _gemForUsds() suffers a rounding error up to approximately 1e12 Usds3S-Maple-L01
MapleSkyStrategy:: gemForUsds() computes gemAmount = (usdsAmount WAD) / (to18ConversionFactor (WAD + tout));. As can be seen, to18ConversionFactor is 1e12 for a Usdc gem, which leads to a rounding up to 1e12 Usds.
Low/InfoAave and SavingsUsds strategies may revert when trying to withdraw all funds3S-Maple-L02
Withdrawing all the Aave or Usds balance from the strategy may revert when there is yield.
Low/InfoMapleSkyStrategy does not always cache the psm and misses underscores3S-Maple-N01
MapleSkyStrategy::gemForUsds() and MapleSkyStrategy::usdsForGem() do not cache the psm nor add trailing underscores to tout and to18ConversionFactor. Implement the fixes to ensure gas savings and correct formats.
Low/InfoDifferences in the strategy implementations that could be fixed3S-Maple-N02
The MapleAaveStrategyStorage contract is missing the State Variables separator comment that is present in other similar contracts like MapleSkyStrategyStorage and MapleBasicStrategyStorage.
Low/InfoERC4626::previewRedeem() may revert, which will DoS MaplePool withdrawals3S-Maple-N03
The EIP4626 interface says for previewRedeem() that it may revert, which will DoS withdrawals in the MaplePool: MAY revert due to other conditions that would also cause redeem to revert.
Low/InfoThe basic strategy does not have slippage control when withdrawing which may lead to arbitrage3S-Maple-N04
MapleBasicStrategy::withdrawFromStrategy() does not send an upper limit on the amount of shares minted, which means an unexpected loss may happen in case of slashing before the withdrawal from the EIR4626 vault.
Low/InfoMapleSkyStrategy::assetsUnderManagement() uses maxWithdraw(), which may return 03S-Maple-N05
MapleSkyStrategy::assetsUnderManagement() uses maxWithdraw(), which according to EIP4626 returns 0 when the withdrawal limit is reached or similar.
Low/InfoMapleSkyStrategy:: setPsm() should set the old Psm's approval to 03S-Maple-N06
MapleSkyStrategy::setPsm() changes Psm, but does not set the old Psm approval to 0. Set the old Psm approval to 0 as it is no longer needed. Addressed in 6c9f85d.
Low/Infotin in the Psm will cause an instant drop in the share price which could be leveraged by Maple Pool users3S-Maple-N07
Currently tin is null in the Psm, but if it was non null, it would instantly lead to a reduction in the totalAssets() in the Maple Pool, as a fee would be taken when converting fundsAssets to Usds in the Sky Strategy,…
Low/InfoThere may not be enough gem(Usdc) in the Psm contract, DoSing withdrawals in the Sky Strategy3S-Maple-N08
The psm contract may not have enough gem (USDC) in the pocket, which could revert and DoS withdrawals. It's possible to change the psm address so the issue can be managed, but it could DoS withdrawals for some time.
Low/InfoUsdc to Usds calculation in the Sky Strategy is slightly different than the Psm Usds Wrapper3S-Maple-N09
In the psmWrapper, the Usds amount from the gem is calculated as usdsInWad = gemAmt18 + gemAmt18 psm.tout() / WAD; , but in the Sky Strategy it is calculated as (gemAmount to18ConversionFactor (WAD + tout)) / WAD; .
Low/InfoThe DaiJoin contract may be caged which will DoS withdrawals forever3S-Maple-N10
The daiJoin contract may be caged, which means exit() is DoSed forever.
Low/InfoWithdrawals in the Maple Pool and Sky Strategy may be DoSed in case the DssLitePsm halts buying3S-Maple-N11
The DssLitePsm, used as part of the Usds Psm wrapper that allows converting fundsAsset to Usds in the Sky Strategy, may be halted by setting tout to type(uint256).max.
Low/InfoAave RewardsController can add Usdc to the rewards list and the strategy has no way to collect the rewards3S-Maple-N12
The Aave strategy does not deal with the reward controller, so it could miss out on rewards emitted in case the fundsAsset is listed in the RewardsController.
Low/InfoInactive or Impaired pool creates arbitrage oportunities3S-Maple-N13
When assetsUnderManagement() decreases, a window of opportunity is created for people to stake in the maple pool very cheaply and then sell for profit when it becomes active again or the protocol admin calls…
Crit/HighTotal ETH, WETH and gameETH are not tracked which will lead to insolvencyH-1
Users may upgrade their towers so they receive gameETH over time which can later be withdrawn for ETH.
Crit/HighSignificant rounding errors due to gameETH not having precisionH-2
In Codeup::addGameETH() and Codeup::reinvest(), gameETH is obtained as gameETH = amount / gameETHPrice, which means there may be rounding errors.
Crit/Hightower.gameETHForWithdraw should be reduced pro-rata when there is not enough ETHH-3
Codeup::withdraw() and Codeup::reinvest() cap the withdrawn amount to the available ETH balance, but always set the tower.gameETHForWithdraw to 0, which means that a user can have a significant amont of gameETH for…
Crit/HighCodeup::claimCodeupERC20() may be forever DoSed by creating the Uniswap pool before it is calledH-4
Uniswap pools may be created without the underlying tokens existence, which means that someone may do this before Codeup::claimCodeupERC20() is ever called, making it revert when UniswapV2Factory::createPair() is called…
MediumCodeup::claimCodeupERC20() may revert whenever the weth balance is very lowM-1
Codeup::claimCodeupERC20() adds liquidity to the Uniswap pool whenever the weth balance is bigger than 1. However, an amount bigger than 1 may still lead to reverts if it is low enough.
Low/InfoMissing key zero value checkL-1
gameETHForWithdrawRate is set in Codeup::constructor() to gameETHPrice / 1000 but a zero check is missing. Add the checkValue() function to gameETHForWithdrawRate = checkValue(gameETHPrice / 1000).
Low/InfoCodeup::claimCodeupERC20() is vulnerable to sandwich attacksL-2
Codeup::claimCodeupERC20() does not set minimum values for adding liquidity or swapping (sets 0) and places a deadline of block.timestamp, which means mev bots may sandwich these calls for profit for the protocol's…
Low/InfoTypos in the codebaseI-1
There are typos in the codebase that could be fixed. The following instances were found: availble, spend, luqidity, spended, emitted (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), hrs (should be mins, 1, 2), totalInvestedBedore.
Low/InfoHardcoded values present in the codebaseI-2
Hardcoded values should be avoided for better readability. The following instances were found: (tokenAmount 10) / 100 (1, 2), Codeup::syncTower(), Codeup::getUpgradePrice(), Codeup::getYield()
Low/InfoAll functions could have a started modifier as they should not be called before the start timeI-3
Only Codeup::addGameETH() checks if the protocol has started in require(block.timestamp startUNIX, NotStarted());, but the same checks could be applied to the remaining functions.
Low/InfoOwnable is unused in CodeupERC20 and could be removedI-4
CodeupERC20 inherits Ownable but does not use any of its functionalities. Remove Ownable as it is not used.
Low/InfoUnused TransferFailed error in CodeupERC20I-5
The TransferFailed error in CodeupERC20 is not used and can be removed. Remove this error.
Crit/HighLack of slippage protection leads to loss of protocol fundsH-1
There is no slippage protection while removing liquidity and swap tokens from AMM. There are 2 intances where slippage protection is missing which are as below: 1.
Crit/HighUsers redeeming early will withdraw Ra without decreasing the amount locked, which will lead to stolen funds when withdrawing after expiryH-10
VaultLib::redeemEarly() is called when users redeem early via Vault::redeemEarlyLv(), which allows users to redeem Lv for Ra and pay a fee.
Crit/HighVaultPoolLib::reserve() will store the Pa not attributed to user withdrawals incorrectly and leave in untracked once it expires againH-11
VaultPoolLib::reserve() stores the Pa attributed to withdrawals in self.withdrawalPool.stagnatedPaBalance instead of storing the amount attributedToAmm.
Crit/HighFlashSwapRouter::emptyReserve() and FlashSwapROuter::emptyReservePartial() functions return incorrect valuesH-2
The protocol deposits RA and CT tokens to an AMM pair, from fees or when users call the depositLv() function.
Crit/HighIncorrect redeemAmount Is Accounted Due To Not Accounting For The Exchange RateH-4
When Liquidating LP , DS and CT are paired , then that amount is used to redeem RA . But the accounting for RA has been done incorrectly since it does not account for exchange rate.
Crit/HighIncoming Redemption Assets not being tracked when repurchase is calledH-5
repurchase() function take redemption asset and gives back depeg swap along with pegged.
Crit/HighUsers will steal excess funds from the Vault due to VaultPoolLib::redeem() not always decreasing self.withdrawalPool.raBalance and self.withdrawalPool.paBalanceH-6
Vault::redeemExpiredLv() calls VaultLib::redeemExpired(), which allows users to withdraw funds after expiry, even if they have not requested a redemption.
Crit/HighWrong accounting of locked RA when repurchasing DS+PA with RAH-7
Users have the option to repurchase DS + PA by providing RA to the PSM. A portion of the RA provided is taken as a fee, and this fee is used to mint CT + DS for providing liquidity to the AMM pair.
Crit/HighAdmin new issuance or user calling Vault::redeemExpiredLv() after Psm::redeemWithCt() will lead to stuck funds when trying to withdrawH-8
VaultLib::liquidatedLp() calls PsmLib::lvRedeemRaWithCtDs(), which redeems ra with ct and ds.
Crit/HighAttackers will steal the reserve from the Vault by receiving ra in FlashSwapRouter::__swapDsforRa()H-9
FlashSwapRouter::swapDsforRa() is called as part of FlashSwapRouter::swapRaforDs() whenever the reserve is sold and the resulting Ra is used to provide liquidity to the Vault by calling…
MediumAdmin will not be able to only pause deposits in the Vault due to incorrect check leading to DoSed withdrawalsM-2
The modifier LVDepositNotPaused in Vault::depositLv() checks [states[id].vault.config.isWithdrawalPaused]( instead of [states[id].vault.config.isDepositPaused]( which means deposits will only be paused if withdrawals…
MediumAdmin will not be able to upgrade the smart contracts, breaking core functionality and rendering the upgradeable contracts uselessM-3
The AssetFactory and FlashSwapRouter inherit the UUPSUpgradeable contract in order to be upgradeable.
MediumWithdrawing all lv before expiry will lead to lost funds in the VaultM-5
VaultLib:redeemEarly() redeems users' liquidity vault positions, lv for Ra, before expiry. After expiry, it is not possible to deposit into the vault or redeem early.
MediumRebasing tokens are not supported contrary to the readme and will lead to loss of fundsM-6
The readme states that rebasing tokens are supported Rebasing tokens are supported with exchange rate mechanism However, only non rebasing tokens such as the wrapped version wsteth are supposed.
MediumProviding liquidity to the AMM does not check the return value of actually provided tokens leading to locked funds.M-7
When providing liquidity to an AMM pair, the protocol specifies both the desired amount of tokens to be provided and a minimum amount to be accepted.
Crit/HighMultiplierContract::proposeMultipleUpdates() updates maxLevel before executing the updateH-1
MultiplierContract::proposeMultipleUpdates() updates maxLevel without going through the UPDATEINTERVAL, which will affect the multiplier MultiplierContract::getMultiplier() right away, without updating the actual…
Crit/HighFunding cap in IDOPoolAbstract::_basicParticipationCheck() is imprecise and can lead to excessive tokens soldH-2
IdoTokens are allocated in IDOPoolAbstract::enableIDORound() up to idoConfig.idoSize.
Crit/HighCancelling rounds after finalizing will lead to incorrect globalTokenAllocPerIDORound trackingH-3
Rounds may be claimed after finalized, in which each claim reduces globalTokenAllocPerIDORound[idoConfig.idoToken].
Crit/HighRank and multiplier of a user can not be set if the user is registered for MetaIDO by the adminH-4
IDOPoolAbstract::adminAddRegForMetaIDO() sets metaIDO.isRegistered[user] = true;, but not the rank and multiplier.
MediumIDOPoolAbstract::withdrawSpareIDO() does not take into account that several rounds may use the same IdoTokenM-1
IDOPoolAbstract::withdrawSpareIDO() checks if contractBal = ido.idoSize, where contractBal = IERC20(ido.idoToken).balanceOf(address(this)).
MediumfyToken contribution limits are incorrect as they compare fyToken and buyToken amounts with idoSize in idoToken unitsM-2
The fyToken contribution limit is enforced as: As can be seen, maxFyTokenFunding is in idoToken units (idoSize refers to idoToken) and globalTotalFunded in fyToken or buyToken units.
MediumIDOPoolAbstract does not deal with yield and gas accrued on BlastM-3
Blast accumulates yield for contracts holding USDB, WETH and ETH and enables contracts to claim some of the gas fees. However, this is not currently dealt with so IDOPoolAbstract will miss out on this.
Low/InfoIncorrect RefundClaim event due to deleting idoConfig.accountPositions[msg.sender]L-1
The refund event is emitted as emit RefundClaim(idoRoundId, msg.sender, pos.amount, pos.fyAmount);. pos is defined as IDOStructs.Position storage pos = idoConfig.accountPositions[msg.sender];.
Low/InfoRounds in IDOPoolAbstract::manageRoundToMetaIDO() can be overridenL-2
IDOPoolAbstract::manageRoundToMetaIDO() does not check if idoRoundClocks[roundId].parentMetaIdoId has already been set, allowing it to be overriden and rounds to be linked to more than 1 metaIDO in the metaIDO.roundIds…
Low/InfoUse of address.transfer instead of the recommended address.call{value: amount}("")L-3
address.transfer is unsafe and may lead to stuck tokens as it forwards 2300 gas, which may run out of gas in the fallback function of the receiver. Additionally, future opcode changes could make it stop working.
Low/InfoNull transfers in IDOPoolAbstract::_depositToTreasury() could be skippedI-1
IDOPoolAbstract::depositToTreasury() transfers pos.fyAmount and pos.amount - pos.fyAmount, but one of them may be 0 if the account only used buyTokens or fyTokens.
Low/InfoIDORoundConfig.idoPrice could have a comment somewhere specifying the units for better readabilityI-2
IDORoundConfig.idoPrice is used when converting from IdoToken to buyToken or fyToken, but there is no indication of the code of its units.
Low/InfoMissing disableInitializers() call in StandardIDOPool::constructor()I-3
The implementation contract StandardIDOPool can be initialized by an attacker, although it can not do anything with it. Call disableInitializers() in the constructor of StandardIDOPool.
MediumBalance check in Vault::withdraw() does not take fees into account3S-OB-M01
Vault::withdraw() transfers funds to an user if address(this)).balance = amount, which may transfer funds resulting from fees.
Mediumaddress.transfer is used in the codebase, which could lead to stuck funds3S-OB-M02
Using address.transfer is not recommended as future gas cost changes can make it no longer work or users might use smart contract wallets.
MediumEquality check should be performed for fees, not >= as it can lead to users sending more fees than supposed3S-OB-M03
In BRC20Factory::burn(), Vault::deposit(), it is required that the fee is equal to or exceeds the msg.value.
Low/InfoOwnership in BRC20Factory could be transferred using a 2 step procedure, similarly to Vault3S-OB-L01
It's recommended to change owner using a 2 step procedure to mitigate the risk of the contract losing ownership due to incorrect address.
Low/InfoToken addresses are not validated in BRC20Factory and Vault, so users can use incorrect tokens3S-OB-L02
BRC20Factory::mint(), BRC20Factory::burn(), Vault::deposit() and Vault::withdraw() allow sending any token, which could lead to problems if the token is not a BRC20. Validate the sent token was created by the factory.
Low/InfoBRC20Factory::burn() could validate the receiver length3S-OB-L03
BRC20Factory::burn() does not validate the receiver, which could lead to wasted funds. It's impossible to do a fail safe verification, but at least the length could be checked.
Low/InfoBRC20Factory::addSigner() and Vault::addSigner() must not allow adding the zero address as a signer3S-OB-L04
The signature verification in BRC20Factory and Vault does not check that the recovered address is not null, which means that if address(0) is added as a signer, it would be possible to forge signatures.
Low/InfoFee event is missing in the constructors of BRC20Factory and Vault3S-OB-L05
The fee is set in the constructors of BRC20Factory and Vault, but the event is not emitted. Emit the FeeChanged event in the constructor of both contracts. Addressed in edbf47e.
Low/InfoBRC20Factory constructor is missing a duplicate check3S-OB-L06
BRC20Factory constructor does not check for duplicate signers, which would lead to problems as the index would be overriden. Check for duplicate signers using the recommendation from 4. Addressed in ea46e07.
Low/InfoDomain separator calculation is not fork safe3S-OB-L07
The domain separator is cached in the constructor of the contracts, which could lead to signature signing softwares in case of a hard fork.
Low/InfoChecks effects interactions pattern is now always followed3S-OB-N01
State changes should always be performed before interacting with an external contract, or it increases the risk of reentrancy attacks. In Vault::withdraw() decrease allowances[token] before sending the funds.
Low/InfoBRC20Factory and Vault do different checks when removing signers3S-OB-N02
BRC20Factory checks if the account is authorized and if its index stored in indexes[account] is smaller than the length of signers.
Low/InfoStorage variables can be cached to save gas3S-OB-N03
Reading/writing from the same storage variable more than once should be avoided to save gas. BRC20Factory::burn() can cache the fee (read twice). Vault::deposit() cache the fee (read thrice).
Low/InfoBRC20 parameters should be passed as arguments to the constructor of BRC20 to save gas3S-OB-N04
BRC20Factory::createBRC20() creates new BRC20 tokens by setting the parameters storage variable to the name, symbol and decimals of the new token, and then the BRC20 token in the constructor fetching this storage…
Low/InfoSigner duplicate check can be performed by requiring signers being sent ordered3S-OB-N05
Signers are checked for duplicates by creating a memory signers array and looping through this array for every new signer, which is inefficient.
Low/Infoauthorized mapping is not required if the indexes mapping stores the indexes + 13S-OB-N06
BRC20Factory and Vault store the signers array, the authorized and indexes mapping.
Low/InfoReentrancy guard can be implemented with a uint256 to save gas3S-OB-N07
Storing a uint256 instead of a bool saves gas as it is cheaper changing storage from non null to non null value. Implement the guard with a uint256. Here is an example implementation from OpenZeppelin.
Crit/HighBasicVault::_redeem() burns newAmount but redeems amount, allowing attackers to drain the vault3S-Mitosis-C01
BasicVault::redeem() is inconsistent as it burns newAmount, the value returned from the hook, but pushes a request to the redeem queue with amount.
MediumBasicVault is incompatible with fee-on-transfer tokens3S-Mitosis-M01
The deposit and redeem functions of the vault take an amount parameter and mint/burn the corresponding amounts assuming a 1:1 ratio.
MediumMetadata is not set in Cap, which is required by the inherited Router to specify gas limit to the mailbox3S-Mitosis-M02
The gas limit of messages must be set in the metadata function, as indicated by the docs. When it is not set, the gas limit defaults to 50000, which may not be enough to handle the epoch increase on the destination.
MediumStrategyExecutor::disableStrategy() disables the wrong strategy3S-Mitosis-M03
StrategyExecutor::disableStrategy() removes a strategy from the enabled array by moving the last element to the to be deleted element and popping.
MediumRedeemQueue::getAvailableResolveRange() incorrectly returns false if from == count - 13S-Mitosis-M04
RedeemQueue::getAvailableResolveRange() returns the indices to resolve a requestor, but returns nothing if from == count - 1.
MediumBasicVault::_redeem() does not correctly deal with a disabled redeem queue after it was enabled3S-Mitosis-M05
BasicVault::redeem() only calls BasicVault::resolveWithIdleBalance() when the redeem queue is enabled, but it should also do it when it is disabled as not enough assets may have been reserved.
MediumBasicVault::_deposit() should always resolve with idle balance as the redeem queue may be disabled with requests pending3S-Mitosis-M06
BasicVault::deposit() only calls BasicVault::resolveWithIdleBalance() if the redeem queue is enabled;
Low/InfoProtocol should disable renouncing ownership if it is never intended3S-Mitosis-L01
Contracts inheriting Ownable2StepUpgradeable or Ownable allow for renouncing ownership, which could be catastrophic if triggered by mistake.
Low/InfoThe StrategyExecutor can remain in a paused state if the owner renounces control while it's paused3S-Mitosis-L02
The StrategyExecutor contract inherits Ownable2StepUpgradeable and PausableUpgradeable, allowing the owner to pause the contract.
Low/InfoBasicVault::manualRedeem() and BasicVault::manualDeposit() are inconsistent3S-Mitosis-L03
BasicVault::manualDeposit() does not pull assets from the msg.sender, but BasicVault::manualRedeem() burns shares from msg.sender, which is inconsistent.
Low/InfoBasicVault::getRedeemRequestOf() will revert as the redeem requests are filled with the wrong indexes3S-Mitosis-L04
In BasicVault::getRedeemRequestOf(), redeemRequests is filled with the requests from the queue from the IndexByRequestor struct offset, which stores the last non resolved request.
Low/InfoCap::receive() does not place restrictions in the sender, which may lead to donations3S-Mitosis-L05
Cap::receive() has no restrictions in place of the msg.sender, which means anyone can send funds to Cap, without any effect. Delete the function or specify who may call it. Addressed in b9ed378.
Low/Info_msgSender() is mixed with msg.sender3S-Mitosis-L06
The code is using OpenZeppelin's Context contract which is intended to allow meta-transactions.
Low/InfoAdopt named mappings for clarity3S-Mitosis-N01
To enhance code clarity, consider employing named mappings. Addressed in dfc8c2c.
Low/InfoLinea does not support PUSH03S-Mitosis-N02
Linea does not support PUSH0, so it may be necessary to compile the contracts with an old em version (such as Paris, which is the one currently used).
Low/InfoBasicVaultFactory::createVault() inconsistent already existing vault check3S-Mitosis-N03
BasicVaultFactory::createVault() reverts with a vault already created error whenever $.vaultIds[vault] != 0 && $.vaults[$.vaultIds[vault]] != vault.
Low/InfoInitializable constracts should call _disableInitializers() in the constructor instead of using the initializer modifier3S-Mitosis-N04
When locking the implementation contract, the usual behaviour is calling disableInitializers().
Low/Infopragma abicoder v2; is turned on by default after a certain solidity version3S-Mitosis-N05
pragma abicoder v2; is not required anymore as it is used by default. Remove pragma abicoder v2;. Addressed in 8b05c08.
Low/InfoRedeemQueue::isResolved() resolvedCount always returns requestIds.length3S-Mitosis-N06
resolvedCount in RedeemQueue::isResolved() always increments in the loop, regardless of it being resolved or not. The function is not exposed so it has no impact but could be a future issue.
Low/InfoRedeemQueue::findOffsetIndex() does not check the last index3S-Mitosis-N07
RedeemQueue::findOffsetIndex() performs binary search but does not check the last index when doing so, as it assigns uint256 r = len - 1;.
Low/InfoStructs in RedeemQueue are out of order and should all be placed at the top3S-Mitosis-N08
The style guide can be found here, and structs should not be placed in between code. Follow the style guide for better readability. New Issue
Low/InfoRedeemQueue could be optimized3S-Mitosis-N09
The current RedeemQueue implementation goes through requests and marks them as resolved one by one.
Low/InfoRedeemQueue::get() reverts due to underflow when it should revert and throw the correct error3S-Mitosis-N10
RedeemQueue::get() reverts due to underflow if index.count == 0 instead of the error IndexOutOfRange. IndexOutOfRange(index.offset, index.count, idx); Addressed in 17568a7.
Low/InfoSolidity types uint256 are never negative3S-Mitosis-N11
BasicVault::deposit() checks if amount is negative, which is unnecessary. Replace <= by ==. Addressed in b13e220.
Low/InfoBasicVault::_resolveWithIdleBalance() can return before calculating _idleBalance() to save gas3S-Mitosis-N12
BasicVault::resolveWithIdleBalance() returns when there is enough reserved amount to fulfill all requests. In this case, there is no need to fetch the idleBalance(), should the order should be inverted to save gas.
Low/InfoStorage variables can be cached to save gas3S-Mitosis-N13
Storage variables should be cached to avoid reading from storage more than once and incurring extra gas costs. In BasicVault::enableEOL(), $.asset.forceApprove(address($v2.eol), type(uint256).max);
Low/Infodecimals() is not part of the ERC20 standard and not all tokens may implement it as expected, which may cause initialize() to fail3S-Mitosis-N14
BasicVault is initialized by calling asset::decimals(), but as it is not part of the standard, it may not be implement the function in the expected way or at all.
Low/InfoCap::_checkRemoteStateAndAdvance() may be optimized by returning early as soon as a different epoch is found in a remote domain3S-Mitosis-N15
Cap::checkRemoteStateAndAdvance() advances the next epoch if all domains are equal to the minimum domain found. Thus, if any of the domains is not equal to the minimum, it will return anyway.
Low/InfoThe domain with 0 index in Cap::_checkRemoteStateAndAdvance() is not checked for equality3S-Mitosis-N16
Cap::checkRemoteStateAndAdvance() returns if not all domains have the same epoch, but the domain with index 0 is skipped in the loop.
Low/InfoThe current epoch will never advance if there are no other domains3S-Mitosis-N17
In Cap::processDone(), it returns if no domains are set, if (domains.length == 0) return;.
Low/InfoCap::setEpochCap() may add a stale cap3S-Mitosis-N18
Cap::setEpochCap() reverts when nextCap < $.cap[i], allowing the next cap to be equal to the current cap, which does not seem intended according to the error reason 'cap should be greater than previous cap'.
Low/InfoIn AggregateHook, several if (newAmount == 0) checks are ambiguous3S-Mitosis-N19
In AggregateHook::reportRedeem(), AggregateHook::previewReportClaim() and AggregateHook::reportClaim(), it sets newAmount to prevAmount if newAmount is null, which is always, as it is initialized by default to null.
Crit/HighVault portion calculation in PrizePool::getVaultPortion() is incorrect as _startDrawIdInclusive has been erasedH-1
The vault portion calculation in PrizePool::getVaultPortion() is incorrect because it fetches a startDrawIdInclusive that has been overwritten in the total accumulator but not in the vaults accumulator or the donation…
Crit/HighDraw auction rewards likely exceed the available rewards, resulting in overpaying rewards or running into an InsufficientReserve errorH-3
The sum of the calculated startDraw and finishDraw reward fractions likely exceeds 1.0, which can lead to overpaying rewards or an InsufficientReserve error.
MediumEstimated prize draws in TieredLiquidityDistributor are off due to rounding down when calculating the sum, leading to incorrect prizesM-10
Estimated prize draws in TieredLiquidityDistributor are off due to rounding down when calculating the expected prize count on each tier, leading to an incorrect next number of tiers and distribution of rewards.
MediumClaimers can receive less feePerClaim than they should if some prizes are already claimed or if reverts because of a reverting hookM-11
If a claimer propose an array of prizes to claim, but some of these prizes have already been claimed, or some claims revert, then the actual feePerClaim received will be less compared to what it should really be, as…
MediumWitnet is not available on some networks listedM-13
Witnet is not deployed on Blast, Linea, ZkSync, Zerion, so draws will not be possible there. Vaults can still be deployed and yield earned, so it will fail when trying to start or complete draws.
MediumDrawManager.canStartDraw does not consider retried RNG requests when determining if a new draw auction can be startedM-14
Inconsistent checks in the DrawManager.canStartDraw function, neglecting to consider retried RNG requests, might lead to wrongly assuming that a new draw auction cannot be started.
MediummaxDeposit doesn't comply with ERC-4626M-16
maxDeposit doesn't comply with ERC-4626 since depositing the returned amount can cause reverts The contract's maxDeposit function doesn't comply with ERC-4626 which is a mentioned requirement.
MediumGas Manipulation by Malicious Winners in claimPrizes FunctionM-19
A malicious winner can exploit the claimPrizes function in the Claimer contract by reverting the transaction through returning a huge data chunk.
MediumPrice formula in TpdaLiquidationPair._computePrice() does not account for a jump in liquidatable balanceM-3
The linearly decreasing auction formula in TpdaLiquidationPair.computePrice() does not account for sudden increases in the vault's liquidatable balance, causing the price returned to be much lower than it should be.
MediumThe claimer's fee will be stolen by the winnerM-5
The user calls claimPrizes to collect prizes for the winner, earning some rewards in the process. However, during this process, the winner can steal the fees collected by the user without paying any gas fees.
MediumPotential ETH Loss Due to transfer Usage in Requestor Contract on zkSyncM-7
- The Requestor contract uses transfer to send ETH which has the risk that it will not work if the gas cost increases/decrease(low Likelihood), but it is highly likely to fail on zkSync due to gas limits.
MediumDoSed liquidations as PrizeVault::liquidatableBalanceOf() does not take into account the mintLimit when the token out is the assetM-9
PrizeVault::liquidatableBalanceOf() is called in TpdaLiquidationPair::availableBalance() to get the maximum amount to liquidate, which will be incorrect when tokenOut is the asset of the PrizeVault, due to not taking…
Crit/High_sendOrEscrowFunds will brick LCG funds causing insolvencyH-10
LenderCommitmentGroup (LCG) will have its funds stuck if transferFrom inside sendOrEscrowFunds reverts for some reason. This will increase the share price but not transfer any funds, causing insolvency.
Crit/HighburnSharesToWithdrawEarnings burns before math, causing the share value to increaseH-2
The burnSharesToWithdrawEarnings function burns shares before calculating the share price, resulting in an increase in share value and causing users to be overpaid.
Crit/HighliquidateDefaultedLoanWithIncentive sends the collateral to the wrong accountH-4
liquidateDefaultedLoanWithIncentive sends the collateral to the Lender - LenderCommitmentGroup (LCG) instead of the liquidator. Liquidators will not be incentivized to liquidate.
Crit/HighAnyone can steal pool shares from lender group if no-revert-on-failure tokens are usedH-5
Anyone can steal pool shares from lender group if no-revert-on-failure tokens are used.
Crit/HighDrained lender due to LenderCommitmentGroup_Smart::acceptFundsForAcceptBid() _collateralAmount by STANDARD_EXPANSION_FACTOR multiplicationH-6
LenderCommitmentGroupSmart::acceptFundsForAcceptBid() multiplies collateralAmount by STANDARDEXPANSIONFACTOR (1e18), allowing users to borrow with 1e18 times less collateral.
Crit/HighLenderCommitmentGroup_Smart picks the wrong Uniswap price, allowing borrowing at a discount by swapping before withdrawingH-7
LenderCommitmentGroupSmart calculates the spot and twap prices of the Uniswap pool and ideally picks the worst price for the user, but this is not the case and the opposite is true.
Crit/HighInterest rate in LenderCommitmentGroup_Smart may be easily manipulated by depositing, taking a loan and withdrawingH-8
LenderCommitmentGroupSmart gets the interest directly from the utilization ratio, which may be gamed and a loan may be taken with lower interest rate at no risk.
MediumIssue #497 'Add parameter to lender accept bid for MaxMarketFee' from previous audit is still presentM-10
Issue 497 from the previous Sherlock audit was not fixed in the current code and is still present.
MediumIncorrect selector in FlashRolloverLoan_G5::_acceptCommitment() does not match SmartCommitmentForwarder::acceptCommitmentWithRecipient()M-11
FlashRolloverLoanG5::acceptCommitment() allows picking the SmartCommitmentForwarder, but the selector is incorrect, making it unusable for LenderCommitmentGroupSmart.
MediumFlashRolloverLoan_G5 will fail for LenderCommitmentGroup_Smart due to CollateralManager pulling collateral from FlashRolloverLoan_G5M-12
FlashRolloverLoanG5 calls SmartCommitmentForwarder::acceptCommitmentWithRecipient(), which will have CollateralManager commiting tokens from FlashRolloverLoanG5, which will revert as it does not approve it nor have the…
MediumFlashRolloverLoan_G5 will not work for certain tokens due to not setting the approval to 0 after repaying a loanM-13
FlashRolloverLoanG5::repayLoanFull() approves TELLERV2 for repayAmount, but TELLERV2 always pulls the principal and interest, possibly leaving some dust approval left.
MediumPerforming a direct multiplication in _getPriceFromSqrtX96 will overflow for some uniswap poolsM-14
The getPriceFromSqrtX96 will revert for pools that return a sqrtPriceX96 bigger than type(uint128).max.
MediumMissing __Ownable_init() call in LenderCommitmentGroup_Smart::initialize()M-2
Ownableinit() is not called in LenderCommitmentGroupSmart::initialize(), which will make the contract not have any owner.
MediumLenderCommitmentGroup_Smart does not use mulDiv when converting between token and share amounts, possibly leading to DoS or loss of fundsM-3
LenderCommitmentGroupSmart calculates the exchange rate and valueOfUnderlying() without using mulDiv from OpenZeppelin, which might make it overflow, leading to DoS and possible loss of funds.
MediumLenderCommitmentGroup_Smart_test::addPrincipalToCommitmentGroup/burnSharesToWithdrawEarnings() are vulnerable to slippage attacksM-4
LenderCommitmentGroupSmarttest::addPrincipalToCommitmentGroup() and LenderCommitmentGroupSmarttest::burnSharesToWithdrawEarnings() are vulnerable to slippage attacks and should set slippage protection, contrarily to the…
MediumAPRs are lower than they shouldM-7
LenderCommitmentGroup (LCG) calculates the APR borrowers can borrow on using getMinInterestRate. However, this math doesn't include the amount that the borrower is currently borrowing.
MediumLenderCommitmentGroup pools will have incorrect exchange rate when fee-on-transfer tokens are usedM-9
LenderCommitGroupSmart contract incorporates internal accounting for the amount of tokens deposited, withdrawn, etc. The problem is that if one of the pools has a fee-on-transfer token, the accounting is not adjusted.
Crit/HighClearingHouseLiq::_assertLiquidationAmount() may increase basis points due to negative quoteBalance.amount + insurance3S-Vertex-C01
If the position is a spread, it may be liquidated as one up to the minimum absolute value of the spot and perp positions.
Crit/Highstate.cumulativeDepositsMultiplierX18 may become 0 or negative, leading to loss of funds3S-Vertex-H01
state.cumulativeDepositsMultiplierX18 may go below 0 if an account is socialized or in the worst case, 0, which will have greater impact.
Crit/HighOffchainExchange::swapAmm() does not validate that txn.priceX18 > 0, allowing donation attacks3S-Vertex-H02
OffchainExchange::swapAmm() calculates the quote amount as -txn.amount.mul(txn.priceX18). Thus, if the price is negative, base and quote amount will have the same sign.
Crit/HighClearingHouseLiq::_finalizeSubaccount() does not check if the subaccount has lps3S-Vertex-H03
ClearingHouseLiq::liquidateSubaccountImpl() can finalize an account, repaying the bad debt from insurance or socializing the account, depending on the amount of insurance available.
MediumMissing disableInitializers() call in the constructor3S-Vertex-M01
When using Initializable.sol, it's a good practice calling disableInitializers() in the constructor, such that the implementation itself can't be initialized.
Low/InfoVerifier::checkQuorum() returns false with more than 3 signers3S-Vertex-L01
The signerBitmask is hardcoded to 7 when calling requireValidSignature(), meaning that nSigned would be incremented at most 3 times in checkQuorum() as 7 = 00000111.
Low/InfoDowncasting Leads to Silent Overflow3S-Vertex-L02
Variables of type int128 are being upcasted to int256 and then downcasted again in the burnLp function in PerpEngineLp.sol and SpotEngineLP.sol, leading to silent overflow.
Low/InfoisHealthy() Function Always Returns True3S-Vertex-L03
The isHealthy function in OffchainExchange.sol consistently returns true regardless of its parameter.
Low/InfoPrecision loss due to engines not using .muldiv() when calculating lp ratios3S-Vertex-L04
For example, in SpotEngineState::getInLpBalance(), the ratio is calculated first, likely incurring in precision loss as lpState.supply is expected to be bigger than 1e18 (used in .div()).
Low/InfoUnused argument in Endpoint::registerTransferableWallet()3S-Vertex-L05
Endpoint::registerTransferableWallet() sets transferableWallets[wallet] to true, but a transferable argument can be provided. Delete the argument or replace true by transferable. Addressed in 38bbe76.
Low/InfoRisk parameters should include additional checks to prevent mistakes3S-Vertex-L06
Risk parameters should be lower, higher or equal to 1e18, depending if short or long. For example, the long risk values should be smaller or equal to 1e18, or some of the functionality may get corrupted.
Low/InfoPerpEngineLp::burnLp() and SpotEngineLp::burnLp() are missing slippage checks3S-Vertex-L07
PerpEngineLp::burnLp() and SpotEngineLp::burnLp() have no slippage control parameters, which means that a user could get base and quote at a worse price/quantity than expected.
Low/InfoClearingHouse::addEngine() initializes productEngine, so it may be frontrunned and someone initializes productEngine instead3S-Vertex-L08
ClearingHouse::addEngine() initializes productEngine, which means it is uninitialized prior to the call.
Low/InfoAny user may enforce the referral code of other new users3S-Vertex-L09
In Endpoint::depositCollateralWithReferral(), the referral is set to referral code to default. It's also possible to do this by submitting a slow mode transaction of type BurnLpAndTransfer.
Low/InfotxType == TransactionType.ExecuteSlowMode may cause batch of transactions to fail due to _slowModeConfig.txUpTo < _slowModeConfig.txCount3S-Vertex-L10
txType == TransactionType.ExecuteSlowMode allows the sequencer to execute slow mode transactions. However, these transactions can also be executed permissionlessly.
Low/InfoUser deposits are exposed to protocol risk before being able to do anything3S-Vertex-L11
depositCollateral() and related functions transfer funds from the user, but only after the delay or the sequencer submission is the balance of the user updated.
Low/InfoRatios in PerpEngineLp or SpotEngineLp may be manipulated when the liquidity is low3S-Vertex-L12
PerpEngineLp::mintLp() assigns quote according to with the requirement that amountBase % sizeIncrement == 0.
Low/InfoReevaluate the Need for renounceOwnership()3S-Vertex-L13
Consider overriding OpenZeppelin's Ownable renounceOwnership() function if the project's roadmap doesn't plan to relinquish total ownership control. Acknowledged
Low/InfoOwnable2Step is preferred over Ownable3S-Vertex-L15
Ownable2Step places some important checks, such as a 2 step ownership transfer procedure and should be preferred over Ownable. Replace Ownable2Step by Ownable whenever possible. Acknowledged
Low/InfoMissing events3S-Vertex-L16
Some state changes are missing events: - ArbAidrop, all functions. Emit events for all relevant state changes. Acknowledged
Low/InfoIERC20Base Noncompliant With ERC20 Standard3S-Vertex-N01
The interface declares increaseAllowance and decreaseAllowance functions, which aren't part of the official ERC20 standard. Calls to such functions may revert with some tokens.
Low/InfoUse of abi.encodePacked() with Dynamic Types3S-Vertex-N02
Using abi.encodePacked() with dynamic types, as seen in Verifier.sol on L160 and in Endpoint.sol on L749, isn't advisable when feeding the outcome into a hashing function like keccak256(), due to hash collisions.
Low/InfoRiskHelper::isoGroup() returns 0, regardless of the passed subaccount3S-Vertex-N03
RiskHelper::isoGroup() is called in various functions across the code such as ClearinghouseLiq::finalizeSubaccount(), and ClearinghouseLiqsettlePositivePerpPnl().
Crit/HighIn StakingAssetManager::lockERC20() the resulting note is created with asset instead of zkToken3S-SG-C01
Users can transfer tokens to the StakingAssetManager in return for a note commitment of the corresponding zkToken.
Crit/HighMerkleRoot is not validated in all StakingAssetManager functions3S-SG-C02
All functions that require a merkle root to verify note inclusion must validate that the merkle root is valid. Otherwise, attackers can forge a merkle root that includes a fake note and steal all assets.
Crit/HighChecks-effects-interations pattern is not always followed, which can be used to drain all tokens3S-SG-C03
Here is a writeup about the pattern. Essentially state changes should be handled before interacting with external contracts (for example, when sending ETH via .call(""), to avoid reentrancy.
Crit/HighCurve multi exchange does not validate assetIn and assetOut against route3S-SG-C04
CurveMultiExchangeAssetManager::curveMultiExchange() receives arguments assetIn, assetOut and route. Route is defined as Array of [initial token, pool or zap, token, pool or zap, token, ...] .
Crit/HighDarkPoolAssetManager can be drained by looping over join(), joinSplit() or swap() with only some initial amount3S-SG-C05
DarkPoolAssetManager::join(), DarkPoolAssetManager::joinSplit() and DarkPoolAssetManager::swap() don't check if the 2 notes are the same. Thus, using only 1 note, it's possible to double the amount of funds available.
Crit/HighStuck ETH in Curve exchanges due to sending msg.value to the exchange instead of amountIn3S-SG-C06
The curve exchanges allow users who deposited to use their note commitments to swap for other assets.
Crit/HighReusing the same rho and pubKey in different deposits leads to lost tokens3S-SG-C07
The nullifier is formed from rho and the schnorr pubKey. This means that depositing with different assets and amounts will still have the same nullifier, leading to the inability to move the following deposits with the…
Crit/HighUniswapLiquidityAssetManager::_validateCollectFeesArgs() validates nullifier instead of note footer3S-SG-H02
UniswapLiquidityAssetManager::validateCollectFeesArgs() allows using the same note footers as it checks for nullifiers, when it should check for note footers. Check for note footers correctly. Addressed in 8b300f0.
Crit/HighCurveAssetManagerHelper::_validateAssets() should check that the number of assets provided is smaller than the maximum of the pool3S-SG-H03
CurveAssetManagerHelper::validateAssets() allows sending assets with indexes bigger than the maximum allowed of a pool (numcoins). This could lead to users losing tokens or unexpected behaviours.
Crit/HighAll curve params should be signed by the schnorr private key in the proof, or users may be griefed3S-SG-H04
An example of this issue is for example here. If the last bit of isLegacy is 0, it checks address(this).balance, otherwise it IERC20(WETHADDRESS).balanceOf(address(this)).
Crit/HighCurve multi exchange can be used to withdraw assets without paying fees3S-SG-H05
CurveMultiExchangeAssetManager::exchange() allows sending any route, which is of type Array of [initial token, pool or zap, token, pool or zap, token, ...].
Crit/HighAnyone can deposit to DarkpoolAssetManager as the owner can be freely chosen without any implication3S-SG-H06
DarkpoolAssetManager::depositETH() and DarkpoolAssetManager::depositERC20() allow choosing any owner, while the msg.sender still receives the deposit.
Crit/HighAttackers can include other users nullifiers to make their funds stuck when adding liquidity to curve3S-SG-H07
CurveLiquidityAssetManager::curveAddLiquidity() always spends the nullifiers, even if the amounts used are 0.
Crit/High_addLiquidity() slippage is incorrectly set3S-SG-H08
The slippage is computed as: This gets the mintAmount value post price manipulation, rendering the slippage protection useless. Also, hardcoding a parameter of 95% slippage is not ideal. Here is a similar finding.
Crit/HighAnyone can frontrun a relayer interaction with the same arguments but a much higher/lower relayer fee3S-SG-H09
The relayer fee used throughout the codebase is not enforced anywhere, such that it's possible to frontrun a legit transaction and set a much lower/higher relayer fee, harming the relayer/user.
Crit/HighDarkpoolAssetManager::Split() into 2 equal amounts leads to lost funds3S-SG-H10
When using DarkpoolAssetManager::Split(), there is no check to make sure that the 2 resulting notes are not equal, making it impossible to use the second note after the first one as the nullifier will be used by then.
MediumService fees should depend on asset3S-SG-M01
Service fees are fixed, but they should depend on the asset, as their price differs significantly. Users are likely to arbitrage this. Set a mapping for service fees. Acknowledged
MediumCurveAddLiquidityAssetManager::curveAddLiquidity() does not deal correctly with isLegacy = 0b10 and ETH3S-SG-M02
If isETH, but args.isLegacy is 1, it wraps to weth and allows pool, see here. if args.islegacy is 0b10 and the asset is ETH, it does not send ETH to the pool, here.
MediumUniswap asset managers are missing slippage checks3S-SG-M03
All Uniswap interactions are missing a deadline argument and are not setting the following slippage protection arguments in UniswapLiquidityAssetManager: - amount0Min and amount1Min in MintParams.
MediumNo support for fee on transfer tokens3S-SG-M04
Fee on transfer tokens are not correctly dealt, as the transferFrom() call is expected to transfer exactly the requested amount. However, this is not the case for tokens that charge a fee on transfer.
MediumSome ETH transfers don't revert if they fail3S-SG-M05
ETH transfers should revert if they fail. Although this issue alone will not lead to exploits, it increases the attack surface. Revert if the transfers fails in: - Curve remove liquidity. - Curve multi exchange.
Low/InfoDecimals in ZKToken are not set to the underlying decimals, which will likely harm tvl calculations in aggregators3S-SG-L01
ZKTokens are minted 1:1 to the underlying assets, so should inherit the same decimals. For example, if USDT is the underlying asset, and 100 USDT are locked, it will mint 100e6 ZKTokens.
Low/InfoStakingOperator::setCollateralToken() will cause issues if it changes tokens relations that have already been set3S-SG-L02
Tokens are locked and unlocked, minting and burning zkTokens in the process, respectively.
Low/InfoCurve pools should be whitelisted as some of them may not be 100% compatible3S-SG-L03
Curve pools vary significantly between one another, which could lead to unexpected behavior.
Low/InfoMissing event in VerifierHub::setVerifier()3S-SG-L04
VerifierHub::setVerifier() should emit an event when a verifier is set. Emit events for all relevant state changes. Addressed in 8b300f0.
Low/InfoERC20AssetPool and ERC721AssetPool should have the nonReentrant modifier as ERC721 and some tokens have callbacks3S-SG-L06
ERC721 implements a callback when transferring tokens. Some tokens, such as ERC777 implement callbacks when transferring. Both these tokens could lead to reentrancy.
Low/InfoGenerating numbers smaller than P by doing % P might be vulnerable3S-SG-L07
1) the hashes are not unique, num A and num B = A + P will yield the same % 2) doing % P introduces a bias in the hash where smaller values are more frequent.
Low/InfoUniswap collect fees should skip collecting fees of one of the tokens if the amount is 03S-SG-L08
Uniswap positions may collect fees in only of the tokens, leading to 0 fees in the other. This will cause the fee manager to revert in calculateFee(). Skip transferring the funds if the amount is 0. Addressed in 8b300f0.
Low/InfoMerkleTreeOperator::getMerklePath() will revert due to OOG after enough elements3S-SG-L09
MerkleTreeOperator::getMerklePath() searches the tree in linear time for the index of the requested noteCommitment. Thus, it will run out of gas or timeout when using rpc providers when enough leaves are added.
Low/InfostakingAssetManager in ZKToken may be immutable as it is never changed3S-SG-N01
stakingAssetManager in ZKToken can be made immutable as it is set in the constructor and never changed. Set stakingAssetManager to immutable to save gas on reads. Addressed in bff8628.
Low/InfoStakingOperator::_setUnlockWindow() checks if the times are negative, which is impossible3S-SG-N02
In StakingOperator::setUnlockWindow(), unlockWindowStart and unlockWindowDuration are uint256, so they can not be negative. However, the code checks for negative values, which is a waste of gas.
Low/InfoStakingOperator does not set isUnlockWindowActive to true in the constructor if the flag passed is true3S-SG-N03
The constructor of StakingOperator is As can be seen, it receives a flag isUnlockWindowActive, but never states the state variable isUnlockWindowActive. This may be intended, but flagging it because it is not clear.
Low/InfoChecks effects interactions pattern is not always followed3S-SG-N04
In StakingAssetManager::unlock(), fees are forwarded before marking the nullifier as used by calling postWithdraw().
Low/InfoVariables are initialized to 0 by default3S-SG-N05
Some variables such as these are initialized to 0, which is not necessary. Consider not initializing the variables as it is not required. Addressed in 8b300f0.
Low/InfoUniswapLiquidityAssetManager::uniswapLiquidityProvision() could return tokenId3S-SG-N06
UniswapLiquidityAssetManager::uniswapLiquidityProvision() could return the tokenId for better verbosity. Return the tokenId. Addressed in 8b300f0.
Low/InfoSpelling errors throughout the codebase3S-SG-N07
Some spelling errors can be found in the codebase: Fix the spelling mistakes. Addressed in 8b300f0.
Low/InfoUnused noteCommitment parameter in UniswapRemoveLiquidityInputs struct3S-SG-N08
UniswapRemoveLiquidityInputs struct in UniswapInputBuilder has a parameter named positionNoteCommitment which is not required. Remove the mentioned parameter. Addressed in 8b300f0.
Low/InfoMissing proof identifier, which could lead to using the same proof in another method3S-SG-N09
There is no identifier in the circuits corresponding to the method that the proof relates to. This means that if 2 circuits have exactly the same inputs, the same proof could be used for more than 1 method.
Low/InfoBaseAssetManager missing 0 address checks in the constructor3S-SG-N11
0 address checks in the constructor are a safety check to ensure addresses are correctly set. The BaseAssetManager, which is inherited by all pool managers, could perform these checks.
Crit/HighIn OstiumTradingStorage, firstEmptyTradeIndex() and firstEmptyOpenLimitIndex() overwrite index 0 if not found3S-OS-C01
OstiumTradingStorage::firstEmptyTradeIndex() and OstiumTradingStorage::firstEmptyOpenLimitIndex() return index 0 if they can not find an available index.
Crit/HighLiquidations can be prevented by updating the SL timeout before it expires3S-OS-H01
Traders who continually update their stop losses before the SL timeout expires will never face liquidation.
Crit/HighUneven getPendingAccFundingFees() leading to wrong funding rate update3S-OS-H02
Ostium uses a velocity funding rate update, so the actual change is the integral of the velocity of the past time.
MediumInability to Close Positions with Significant Positive PnL3S-OS-M01
Presently, when there isn't adequate liquidity in the OstiumVault to cover profits for trades with substantial positive PnL, a revert occurs.
MediumMissing Pausability Check in OstiumTrading::executeAutomationOrder() for Opening Orders3S-OS-M02
The execution of a limit order within the OstiumTrading::executeAutomationOrder() function lacks a check to determine if the contract is paused. Addressed in fa95bb0.
MediumOstiumTrading::topUpCollateral() is missing pairsStored.groupMaxCollateral(pairIndex) check3S-OS-M03
When opening trades it is checked if the collateral is within limits for each pair in OstiumTradingCallbacks::withinExposureLimits(). The same should be done in OstiumTrading::topUpCollateral().
MediumOstiumTrading::closeTradeMarket() and OstiumTrading::topUpCollateral() are missing pending trigger checks3S-OS-M04
OstiumTrading::closeTradeMarket() and OstiumTrading::topUpCollateral() can be used to frontrun liquidation calls whose trigger has already been set, making the liquidation fail in OstiumPriceUpKeep::performUpkeep(),…
MediumOracle fees should be payed upfront to protect the protocol from failed performeUpkeep() calls3S-OS-M05
At the moment oracle fees are payed after the trades have been finalized, either in registerTrade(), openTradeMarketCallback() or closeTradeMarketCallback().
MediumError in OstiumPairsStorage::groupMaxCollateral() calculation3S-OS-M06
The calculation of the maximum deposited collateral per group, erroneously divides by 100, assuming it refers to 100%. However, the maxCollateralP variable, specified here, has a precision of 2 decimals.
MediumupdatePair() is missing the pairOk() modifier3S-OS-M07
OstiumPairsStorage::updatePair() updates a pair's attribute but does not check the new values. Add the pairOk() modifier to updatePair(). Addressed in 7fe937e.
MediumOstiumPriceUpKeep::performUpKeep() does not correctly handle possible reverts3S-OS-M08
OstiumPriceUpKeep::performUpKeep() should unregister pending market orders or disable triggers and pending limit orders if the execution fails. However, it is not dealing with all the revert scenarios, for example: 1.
MediumTrader Can Set Wrong TP and SL3S-OS-M09
In OstiumTrading::updateSl(), a validation is present for the maximum stop loss (SL) distance. However, there is no validation for the opposite scenario, as seen in OstiumTrading::openTrade() at L202.
MediumUSDC Blacklisting Prevents Trader Liquidation3S-OS-M10
USDC incorporates a blacklist functionality that reverts any attempt to transfer funds to a blacklisted address, even if the transfer amount is 0.
MediumCasting from int256 to uint256 won't revert if the number is negative, possibly leading to issues3S-OS-M11
Some instances, namely in OstiumPairInfos::getPendingAccFundingFees(), cast int256 to uint256. Avoid casting directly and use a wrapper library instead such as SafeCastUpgradeable. Addressed in 38b09ab.
MediumSetting maxFundingFeePerBlock to a lower value than abs(lastFundingRate) will brick getPendingAccFundingFees()3S-OS-M12
getPendingAccFundingFees() computes the number of blocks to the limit by subtracting absLastFundingRate to maxFundingFeePerBlock. setMaxFundingFeePerBlock() allows setting the max to any value below MAXFUNDINGFEE.
Low/InfoOstiumTradesUpKeep Triggers Automation for Pending Orders Expected to Fail3S-OS-L01
In OstiumTradesUpKeep::getOpenOrdersToTrigger(), orders are grouped together for execution without some important checks.
Low/InfoLimitations for Users Employing Multi-sig or Account Abstraction Wallets in Setting Delegate Address3S-OS-L02
Within the Delegatable::setDelegate() function, there exists a check to determine if a contract is executing the function, restricting the action solely to Externally Owned Accounts (EOAs).
Low/InfoexecuteAutomationOpenOrderCallback Executes STOP Orders at a Worse Price3S-OS-L03
The condition executed when the OpenOrderType is of type STOP, as seen here, triggers when the current price is greater than or equal to the target price.
Low/InfoutilizationThresholdP of 10_000 will make OstiumPairInfos::_getUtilizationOpeningFee() divide by 03S-OS-L04
utilizationThresholdP should never be set to the max 10000 or it will lead to division by 0 when calling OstiumPairInfos::getUtilizationOpeningFee().
Low/InfoOstiumVault::lockDiscount() may revert due to division by 03S-OS-L05
OstiumVault::lockDiscount() reverts if maxDiscountThresholdP == uint16(100) PRECISION2, but the constructor and updateMaxDiscountThresholdP() allow setting it to this value.
Low/InforeqID_pendingAutomationOrder stores the index of the trade, which could point to a different trade since the request was created3S-OS-L06
reqIDpendingAutomationOrder store the trader, pairIndex and index.
Low/InfoOstiumTradingCallbacks::executeAutomationOpenOrderCallback() opens limit orders at market price instead of the limit price3S-OS-L07
Limit orders usually trade at the specified price; however, OstiumTradingCallbacks::executeAutomationOpenOrderCallback() registers the limit order t.openPrice at priceAfterImpact, which would be different than the…
Low/InfoOstiumPairsStorage::getAllPairsMaxLeverage() reverts if enough pairs are created3S-OS-L08
OstiumPairsStorage::getAllPairsMaxLeverage() reverts if enough pairs are created due to OOG.
Low/InfoOwnable2Step is recommended over Ownable3S-OS-L9
Ownable uses a dangerous pattern of setting the address without proper checks. This means that if a mistake is made and ownership is transferred to the wrong address, the owner functionalities would be forever lost.
Low/InfoOstiumRegistry should disable renouncing ownership if it is never intended3S-OS-L10
OstiumRegistry allows renouncing ownership by inheriting Ownable, which could be catastrophic if triggered by mistake. Consider overriding renounceOwnership() and revert if called to disable this functionality.
Low/InfoOstiumLinkUpKeep:topUp() is missing a length check for registryAddresses3S-OS-L11
expected to be equal, as seen in OstiumLinkUpKeep:getUnderfundedUpkeeps(). Either do: or use an array of structs containing the 3 parameters. Addressed in ae32282.
Low/Info.values() may revert when calling getWatchList() due to OOG3S-OS-L12
If the number of registries in the sregistries set grows too large, it will revert when calling getWatchList() due to OOG. Add a paginated function to get the watch list. Acknowledged
Low/InfoOstiumTradingCallbacks::executeAutomationOpenOrderCallback() reverts if it can not find the openLimitOrder3S-OS-L13
OstiumTradingCallbacks::executeAutomationOpenOrderCallback() fetches the openLimitOrder before checking for its existence, which will end up reverting if it does not exist, not finishing execution.
Low/InfoOstiumTrading::executeAutomationOrder() and _getLimitOrdersToTrigger() should check isPaused for open limit orders3S-OS-L14
OstiumTrading::openTrade() correctly checks isPaused to not allow opening positions when the protocol is paused.
Low/InfoIn OstiumLinkUpKeep, empty watchlist or keeperIds arguments are not handled3S-OS-L15
In OstiumLinkUpKeep, functions setWatchList(), addToWatchList() and removeFromWatchList() don't handle 0 length array inputs, which could lead to incorrect state. Explicitly check for empty arrays. Addressed in c9ebe98.
Low/InfoUsing transfer() instead of call() may revert3S-OS-L16
When withdrawing ETH in OstiumPriceUpKeep::withdrawEth() using the deprecated transfer() function will make the transaction revert when: 1. The claimer smart contract does not implement a payable function. 2.
Low/InfoMissing disableInitializers() call in the constructor3S-OS-L17
When using Initializable.sol, it's a good practice calling disableInitializers() in the constructor, such that the implementation itself can't be initialized. Use: Addressed in 1a1c899.
Low/InfoImplement Storage Gap in Delegatable Contract3S-OS-N01
If additional variables are added to the Delegatable contract, conflicts may arise with the storage slots previously written on the proxy. To mitigate this issue, insert a storage gap in the contract: Acknowledged
Low/InfoUnused Trade Size Variable in OstiumTrading::executeAutomationOrder()3S-OS-N02
The leveragedPos variable in OstiumTrading::executeAutomationOrder() is calculated on L443 and L465, but its value isn't utilized anywhere in the function. Addressed in 27e6f2c.
Low/InfoImplement Custom Errors Instead of require Statements3S-OS-N03
The usage of require statements instead of custom errors is evident in both OstiumLinkUpKeep and particularly in OstiumVault.
Low/InfoInconsistent Precision of Percentage Variables3S-OS-N04
Throughout the codebase, variables representing percentages maintain a precision of 2 decimal places.
Low/InfoUnnecessary Typecasting of msg.sender3S-OS-N05
Unnecessary typecasting of msg.sender to address is seen on L34 in OstiumLinkUpKeep::onlyGov() and on L32 in OstiumRegistry::onlyGov(). This redundancy is unnecessary as msg.sender already returns an address.
Low/InfoUse of Magic Numbers3S-OS-N06
Several instances of magic numbers are present throughout the contracts: - The validation for slippageP in OstiumTrading::openTrade() on L169. - L117 in OstiumOpenPnl::getOpenPnl().
Low/InfoUnused and Wrong Imports3S-OS-N07
Unused Imports import '@openzeppelin/contracts/utils/Address.sol'; '@chainlink/contracts/src/v0.8/automation/interfaces/ILogAutomation.sol'; // duplicate import './interfaces/IOstiumOpenPnl.sol';
Low/InfoRedundant Calculations in OstiumTrading::openTrade()3S-OS-N08
When opening a LIMIT or STOP order, the first empty index is calculated using OstiumTradingStorage::firstEmptyOpenLimitIndex() on L209.
Low/InfoOstiumTrading::closeTradeMarket() Makes Redundant Call to OstiumTradingStorage::getOpenTradeInfo()3S-OS-N09
At L284 in OstiumTrading::closeTradeMarket(), the trade info can be reused from the previous call to OstiumTradingStorage::getOpenTradeInfo() at L265 to conserve gas by avoiding redundant function calls.
Low/InfoOstiumLinkUpKeep Config Not Set In initialize()3S-OS-N10
The variable sconfig is not set in the initializer function, which is preferable.
Low/InfoOstiumPriceUpKeep Verifies Reports Using Native Token Instead of LINK3S-OS-N11
The Chainlink documentation states that verifying data streams via native blockchain gas tokens and their ERC20-wrapped versions incurs a surcharge when compared to LINK payments.
Low/InfoOstiumTrading::updateOpenLimitOrder() Makes Unnecessary Field Updates3S-OS-N12
In OstiumTrading::updateOpenLimitOrder(), unnecessary fields of the OpenLimitOrder struct are updated.
Low/InfoOstiumTrading::canExecute() should also be checked in OstiumTradesUpKeep::checkCallback()3S-OS-N13
Orders in OstiumTrading::executeAutomationOrder() will not execute if they are in the timeout period.
Low/InfoUnused UPDATE_SL OstiumPriceUpKeep::OrderType3S-OS-N14
Enum OrderType in IOstiumPriceUpKeep has a UPDATESL property which is never used. Remove this property if it is not used. Addressed in ab4e1ae.
Low/InfoWrong decoding of verifierResponse in OstiumPriceUpKeep::performUpkeep()3S-OS-N16
OstiumPriceUpKeep::performUpkeep() decodes the verifierResponse setting an expiresAt as uint192, when it is in fact a uint32 in both Basic and Premium reports.
Low/InfoupdateGroupCollateral() should revert if the _pairIndex does not exist3S-OS-N17
OstiumPairsStorage::updateGroupCollateral() does not check for the pair's index existence, which means that it would update group with index 0 incorrectly.
Low/InfoError parameters should be more descriptive3S-OS-N18
Occurences OstiumLinkUpkeep, WrongParams. Acknowledged
Low/InfoFaulty revert reason decoding in Delegatable3S-OS-N19
Delegatable decodes the revert reason of a revert in a way that may fail. It adds 4 bytes to the result pointer, which may mess up the length of the result variable, see more details here. Don't decode the reason.
Low/InfoOstiumOpenPnl::average() can be simplified by adding a state variable that tracks the cumulative sum of nextEpochValues3S-OS-N20
OstiumOpenPnl::average() calculates the average of all the nextEpochValues, which is an expensive operation (depending on requestsCount and could be replaced by a state variable tracking the cummulative sum. Acknowledged
Low/InfoOstiumOpenPnl::nextEpochValuesRequestCount stores the same information as nextEpochValues.length3S-OS-N21
OstiumOpenPnl::nextEpochValuesRequestCount can be removed as it is the same as checking nextEpochValues.length. Acknowledged
Low/InfoRedundant abi.decode() in OstiumTradesUpKeep::checkCallback()3S-OS-N22
The second decoding on L126 of the data parameter is identical to the first and does not assign its result to any variable. This appears to be redundant and serves no discernible purpose.
Low/InfoConsider using forceApprove instead of safeApprove in OstiumTraidingCallbacks3S-OS-N23
safeApprove should only be called when setting an initial allowance, because it reverts when a non-zero approval is changed to a non-zero approval.
Low/InfoOstiumLinkUpKeep:removeFromWatchList() complexity could be reduced3S-OS-N24
OstiumLinkUpKeep:removeFromWatchList() currently iterates over the full sregistryWatchLists to find the keeperIds and delete them if equal. This has O(n^2) complexity, which could cost a significant amount of gas.
Low/InfoTypos in OstiumLinkUpKeep3S-OS-N25
OstiumLinkUpKeep has typos in errors WhatchlistEmpty , HasWhatchlist and NoWhatchlist. Addressed in 119db4a.
MediumNotional difference in AmmRouter:removeLiquidty() may revert in certain cases3S-NFTPerp-M01
AmmRouter:removeLiquidity() subtracts the long notional by the short notional if the size is bigger than 0 (contrary if size is smaller than 0), which may revert if the short notional is bigger than the long one.
MediumIt's possible to mint an infinite number of shares without increasing quote or base amounts, due to rounding down3S-NFTPerp-M02
For very low notional amounts, when adding liquidity, invariantIncrease is always bigger than 0 (not true for the first depositer though), but the corresponding quote and base amounts might be 0.
MediumPrice deviation as is can be circumvented by making smaller trades in a loop3S-NFTPerp-M03
The price deviation check will look at the last price snapshots; however, it does not actually enforce that these are from past blocks.
Low/InfoCall _updateMarkPrice() whenever markPrice is changed3S-NFTPerp-L01
Functions addLiquidity() and removeLiquidity() change mark price but don't call updateMarkPrice().
Low/Info_match() always checks the trigger of the first order of a certain tick, instead of checking i order3S-NFTPerp-L02
PositionManager:match() looks for all the orders for their trigger values as some order may have been filled/deleted.
Low/InfoUse a flag to increase or decrease the index of the pool instead of a heuristic3S-NFTPerp-L03
AmmRouter:shitPool() increases the index if the price is within 100 of the upper bound of the current pool price.
Low/InfoPositionManager:_reversePosition() calculates the notional to reverse but could just use the exchangedQuote3S-NFTPerp-N01
PositionManager:reversePosition() reduces the notional by the amount required to close the previous position, fetching the value using the getters before.
Low/InfoDuplicate size to fill check in _staticFillToAmm()3S-NFTPerp-N02
staticFillToAmm() checks twice the size to fill. Remove the second check as it is never triggered. Addressed in 1c107ae
Low/Infogap is missing the private keyword3S-NFTPerp-N03
The abstract contracts use gaps to change storage later if required, but AMMRouterBase is missing the private keyword. Addressed in 1b573fa
Low/InfoIndex underflow is not protected against, although it has no impact as the pool with index type(uint256).max should not be registered3S-NFTPerp-N04
The following places do an unchecked increment/decrement, but the index could underflow and become type(uint256).max. This has no consequences with the code as is, but it's better to revert for underflows. Acknowledged
Low/InfoIn AmmRouter:liquidateMaker(), the pools array can safely be deleted from storage3S-NFTPerp-N05
AmmRouter:liquidateMaker() calls unregisterPoolsFromMaker() with all the pools, which means that it could simply delete the pools from storage. Addressed in 0ea5723
Low/InfotrimDuplicatePools() is very expensive, having O(n^2) complexity and could be simplified3S-NFTPerp-N06
ammRouter:trimDuplicatePools() eliminates duplicate pools in the input by comparing every element of the array for duplicates, which is very expensive.
Low/InfoAdd a 0 address check on the pool when adding liquidity for greater verbosity3S-NFTPerp-N07
AmmRouter:addLiquidity() does not have an explicit check regarding the existence of the pool with the given id, making it only revert when calling poolsAdded[i].addLiquidity() due to internal EVM checks.
Low/InfosetPools() could use more validation3S-NFTPerp-N08
AmmRouter:setPools() is an admin function but could still benefit from input validaiton to prevent mistakes.
Low/InfoMisleading error name3S-NFTPerp-N09
Function closePosition() of the ClearingHouse contract checks if the size sent as an argument to this function is smaller than the size of the position to close.
Crit/HighDistribute is permissionless, allowing malicious users to specify 0 slippage and sandwich the swap3S-TR-H01
distribute() uses the argument amountOutMinimum as slippage control, but the function is permissionless, so malicious users can trigger swaps with 0 minimum amount out. Set up a keeper role to distribute rewards.
MediumStuck tokens due to not using SafeTransfer3S-TR-M01
reclaimToken() does not use SafeERC20.safeTransfer(), which could lead to stuck tokens. Acknowledged.
MediumSwaps should use the deadline argument on top of minimumAmountOut3S-TR-M02
Swapping with block.timestamp as deadline means validators may do MEV up to the minimumAmountOut. Addressed by disabling taxes.
MediumOn tax sells, swaps are vulnerable to MEV3S-TR-M03
Tax sells specify a minimumAmountOut of 0, which is vulnerable to MEV. Keep in mind that this is usually a design choice of tax tokens to avoid other issues. Addressed by disabling taxes.
MediumcreateAmmPairWith() in initialize() will revert if the pair already exists3S-TR-M04
createAmmPairWith() in initialize() reverts if the pair exists, which means that the contract could be DoSed. Check if the pair already exists and skip creating if it does.
MediumAmounts to distribute are added directly to tokenPerShare, skipping using rewardPerToken3S-TR-M05
Rewards are emitted at a rate of rewardTokenPerBlock. However, when distributing, the received reward tokens are added directly to rewardPerToken, instead of being left in the contract and letting rewardTokenPerBlock…
MediumPossible to mint or burn infinite shares, stealing all rewards or making transfers fail3S-TR-M06
Increasing shares infinitely allows an attacker to steal all rewards. It's possible by doing: 1.
Mediumaddress(0) can be added as a pair, triggering taxes on burn3S-TR-M07
A pair with zero address may be added to the pairs mapping which means burns will be taxed. Revert if the pair does not exist in recordAmmPairWith(). Addressed by disabling taxes.
Low/InfototalShares update should add before reducing to avoid underflows3S-TR-L01
totalShares update subtracts before summing, which could lead to underflow in edge cases (when something unexpected happens). Acknowledged.
Low/InfoUsers may send tokens before the dead block period to an address to blacklist it3S-TR-L02
During the dead block period, before trading has started, users may send tokens to an address to maliciously blacklist it.
Low/InfoNative transfers should use Openzeppelin's .sendValue()3S-TR-L03
Paying fees to the marketing operator uses .transfer(), which only forwards 2300 gas, which could lead to reverts if the operator has any logic in the fallback or receive functions.
Low/InfoMissing events3S-TR-L04
Occurrences Acknowledged.
Low/InfoOwnable2Step is preferred over Ownable3S-TR-L05
Openzeppelin Ownable2Step changes transferOwnership() to a 2 step procedure, which means that to change an owner, first a pending owner is set and only then this account must accept ownership.
Low/InfoStorage variables can be cached3S-TR-N01
Occurrences Acknowledged.
Crit/HighLack of deadline in PowerToken.buy can lead to user's cashToken being distributed through ZeroToken holders3S-M^0-H01
The function PowerToken.buy is used to purchase tokens in auction. The price of those tokens decreases with time, following a dutch auction model.
Crit/HighValidator signature with zero timestamp can always be replayed3S-M^0-H02
When calling MinterGateway.updateCollateral, an array of timestamps gets passed. That is because the digest signed by a validator includes an updating timestamp.
MediumMToken's total principal invariant can be broken3S-M^0-M01
Because there's an unchecked total earning principal addition in MToken.addEarningAmount, the MToken contract would be subject to a potential silent overflow of principalOfTotalEarningSupply.
MediumValidator signatures with greater timestamps can be reused in a subsequent updateCollateral3S-M^0-M02
When calling MinterGateway.updateCollateral, an array of timestamps gets passed. That is because the digest signed by a validator includes an updating timestamp.
Low/InfoMultiplication after division in StableEarnerRateModel leads to loss of precision3S-M^0-L01
In the StableEarnerRateModel.getSafeEarnerRate function, there's a log calculation detailed in a comment: The code implementation of the log argument calculation follows the comment, but using fixed point arithmetic…
Low/InfoA decrease in updateCollateralInterval will lead to unfair penalties3S-M^0-L02
The MinterGateway.updateCollateral function should be called once every updateCollateralInverval. If the minter fails to do this a penalty will be imposed on him next time he tries to update his collateral.
Low/InfoUnhandled rounding error in DistributionVault.getClaimable leads to locked dust3S-M^0-L03
The DistributionVault.getClaimable function rounds down during the division operation used to calculate claimable, resulting in dust amounts being locked in DistributionVault.
Low/InfoCreating a new proposal in StandardGovernor may reach a state of permanent DoS3S-M^0-L04
The function StandardGovernor.propose is used to create a new proposal to be executed in the StandardGovernor.
Low/InfoMToken's total principal invariant doesn't hold without MinterGateway, leading to potential principal loss3S-M^0-L05
In function MToken.mint, when the account is an earner, the principal gets added to both its raw balance and to the global variable principalOfTotalEarningSupply.
Low/InfoUnrealized inflation calculation returns wrong value when balance reaches cap3S-M^0-L06
The function EpochBasedInflationaryVoteToken.getUnrealizedInflation calculates a given account's inflation which has not been added to their balance yet.
Low/InfoCustom error for overflowing the total principal is not raised3S-M^0-L07
In the MToken.mint function, there's a check to make sure the total principal doesn't become larger than a uint112: if ( principalOfTotalEarningSupply + getPrincipalAmountRoundedDown(totalNonEarningSupply) =…
Low/InfoProposals in the same voting period can have different ids but do the same3S-M^0-L08
In the BatchGovernor contract, a new proposal will generate a proposal id by using the ZeroGovernor).
Low/InfoFunction cancelMint can be frontrunned to grief a validator3S-M^0-L09
In the MinterGateway.cancelMint function, an approved validator cancels a mint proposal by passing the minter address and mintId. The function will revert if the the minter's proposal doesn't correspond to the input id.
Low/InfoStandardGovernor's implementation of quorum is incompatible with Tally3S-M^0-L10
The StandardGovernor contract inherits from IGovernor, which states the following: But the StandardGovernor contract implements quorum functions in the following way: The documentation from Tally states that it "needs…
Low/InfoA sufficiently large collateral may break the maximum owed M calculation3S-M^0-L11
In the MinterGateway contract, the updateCollateral function can set a collateral value to any value, provided the value is no larger than type(uint240).max and the data is signed by enough validators.
Low/InfoMissing override keyword for interface inherited methods3S-M^0-N01
Most contracts are not implementing their interface methods with override keywords.
Low/InfoSome contracts don't implement their entire interface3S-M^0-N02
There are some contracts that don't fully implement the interface with the same name. For example, IERC5805 defines functions such as delegates, getPastVotes and getVotes.
Low/InfoNo need to set isActive to false if that mapping entry was deleted3S-M^0-N03
In MinterGateway.deactivateMinter the minter's entry in minterStates is deleted using the This action sets all types of that specific entry to its default value, and so, there is no need to set isActive to false as this…
Low/InfoUnnecessary Recomputation of Storage Pointer in MToken._startEarning3S-M^0-N04
In MToken.startEarning function, the balances mapping is stored in the mBalance variable. MBalance storage mBalance = balances[account];
Low/InfoUnnecessary check in StandardGovernor.state3S-M^0-N05
Since StandardGovernor.votingPeriod function will always returns 0, voteStart will always be equal to voteEnd.
Low/InfoCode should not panic underflow3S-M^0-N06
There's several places in the protocol where proper code validation is missing by, instead, relying on panic errors.
Low/InfoStartedEarning event is emitted even if account is already earning3S-M^0-N07
In MToken.startEarning the StartedEarning event will be emitted even if the calling account is already earning: The same happens in the MToken.stopEarning.
Low/InfoWrong comment in Standard Governor's execute function3S-M^0-N08
The following comment is present in StandardGovernor's execute function: // Proposals have voteStart=N and voteEnd=N, and can be executed only during epochs N+1 and N+2.
Low/InfoUnnecessary currentEpoch zero check in StandardGovernor and ThresholdGovernor3S-M^0-N09
The currentEpoch value is derived from BatchGovernor.clock, which can never be zero. But the execute functions of both StandardGovernor and ThresholdGovernor check if currentEpoch is zero.
Low/InfoOverflow check in PowerToken._divideUp is unnecessary3S-M^0-N10
The PowerToken.divideUp function checks if a result of a multiplication "wrapped" around the maximum number, i.e.
Low/InfoUnnecessary conditional check in ThresholdGovernance.execute3S-M^0-N11
The function ThresholdGovernor.execute executes a successful proposal.
Low/InfoInconsistent naming of function in PureEpochs3S-M^0-N12
The PureEpochs library implements a variety of functions centered on the definition of epochs.
Low/Info_checkAndIncrementNonce in ERC5805 raises a ReusedNonce error for non used nonces3S-M^0-N13
The ERC5805.checkAndIncrementNonce function makes sure the nonce being used in a signature is the right one. If it is, it increments it: nonces[account] = currentNonce + 1; // Nonce realistically cannot overflow.
Low/InfocastVoteWithReason always fires a VoteCast event with an empty reason3S-M^0-N14
The BatchGovernor contract supports castVoteWithReason to be compatible with certain governors.
Low/InfotransferFrom in ERC20Extended will always emit an Approval event if the allowance changes3S-M^0-N15
The ERC20Extended.transferFrom function is using approvefor changing allowance, and this internal function always fires up an Approval event.
Low/InfoEIP712's _revertIfError should use all SignatureChecker.Error errors3S-M^0-N16
Function ERC712.revertIfError goes through the different error possibilities in SignatureChecker.Error and raises the right error accordingly. But some are missing: InvalidSignatureS and InvalidSignatureV.
Low/InfoThe IERC3009 interface is not fully conforming to the standard3S-M^0-N17
The IERC3009 defines a token interface following the EIP3009 standard.
Crit/HighreduceOnly limit order update does not take into account that a new amm may be selected, which may lead to loss of funds3S-NFTPerp-C01
On a limit order update, in updateLimitOrder(), changing amm is not being taken into account.
Crit/HighRealized pnl not returned to trader on _mergeDecrease()3S-NFTPerp-H01
On the merge of a liquidator position following a liquidation, the realizedPnl from the previous position is not added to the marginToRemove() variable, but it is removed from the openNotional, leading to loss of yield…
Medium_getPriceToTick() reverts if the price is smaller than 1e10, which may happen in _search() as it assumes the final tick as going entirely to the amm3S-NFTPerp-M01
search() calculates the final tick as if the order is completely filled on the amm.
MediumsetFundingPeriod() can be DoSed due to calling settleFunding()3S-NFTPerp-M02
setFundingPeriod() calls settleFunding() in the ClearingHouse. It may be the case that someone frontruns setFundingPeriod() with a call to settleFunding(), making the setFundingPeriod() transaction revert.
Low/InfoIn fillPair(), when the taker is the maker, the reduceOnly order mapping is not being deleted3S-NFTPerp-L01
When filling limit orders, if the trader doing the openPosition() call is the same as the maker of the limit order, it will delete the limit order.
Low/Info_getPositionNotional() crops the size of the position to the available reserves, which may lead to unexpected profits/losses3S-NFTPerp-L02
getPositionNotional() fills positions to the amm if it can't find limit orders.
Low/InfogetTriggerOrders() should be paginated or it may revert if enough orders are created3S-NFTPerp-L03
There is no limit in the amount of triggerOrders, which means that the array may grow too large and make transactions revert due to the gas usage exceeding the block gas limit when calling getTriggerOrders().
Low/InfoPartially removing liquidity may underflow if the price of the pool has changed significantly3S-NFTPerp-L04
removeLiquidity() updates the position quote and base amount based on the removed quote and base amounts. However, if the proportions changed since the position was opened, it may underflow.
Low/InforemoveLiquidity() in the amm calculates marginToRemove without updating the margin with the funding payment3S-NFTPerp-L05
removeLiquidity() removes the margin from the position pro-rata to the removed shares, without considering the previous funding payment.
Low/InfoPriceFeed: Two step owner transfers are safer3S-NFTPerp-L06
When changing contract owner, a two step process is recommended. Using this method, first a pending owner is set, which then has to accept the role in order to become the contract owner.
Low/InfoTotal Position Size isn't updated properly3S-NFTPerp-L07
The totalPositionSize mapping is used to hold the position information for an amm using the TotalPositionSize struct, which has the following fields: - int256 netPositionSize - uint256 positionSizeLong - uint256…
Low/InfoState changes should always emit events3S-NFTPerp-N01
Some state changes are not emitting events - - Addressed in multiple commits
Low/InfoSwapping amounts that would send the price below/above the bounds of the amm underflows without a reason3S-NFTPerp-N02
For example, getBaseValue() calculates the base amount according to the k using the virtual reserves;
Low/InfoPriceFeed: First owner isn't set as valid keeper3S-NFTPerp-N03
When changing contract owner, the keeper permission of the previous owner is revoked, and this permission is given to the new owner.
Low/InfoStructs can be packed to save gas3S-NFTPerp-N04
In solidity, structs can often be packed into fewer words to save gas on storage loads/stores. In the code, the following structs can be optimized in this way: LimitOrder, TriggerOrder, TwapInputAsset, MarketOrder.
Low/InfoContracts should inherit their interfaces3S-NFTPerp-N05
To avoid differences between the function declarations in the contracts and their interfaces, it is a good practice for contracts to inherit their interfaces.
Low/InfoBug in _reversePosition() might lead to future exploits3S-NFTPerp-N06
In the positionManager library, internal functions to increase, decrease, close or even reverse a position tend to not revert and instead rely on a response system to inform the parent function if the call was…
Low/InfoUsing low level pop is not recommended3S-NFTPerp-N07
Using assembly blocks in solidity isn't recommended, unless it presents as obvious advantage, usually an optimization, that can't be performed with standard solidity.
Low/InfoErrors could include relevant arguments whenever possible, making it easier to debug3S-NFTPerp-N08
While during testing it's possible to get the relevant information using other methods (console.log comes to mind), when the contract is deployed and transactions are live, it's harder to debug without the information…
Low/Infomul() and div() in NFTPMath are misleading due to scaling by 1e18 underneath3S-NFTPerp-N09
Usually mul() and div() do multiplication and division, respectively, without scaling by 1e18 (commonly used by SafeMath). However, NFTPMath mul() and div() scale the operations by 1e18, misleading code readers.
Crit/HighAnyone can grief users, stopping them from fulfilling their withdrawals3S-Clip Finance-H01
On the Batch.sol contract, anyone can call withdrawFulfill() with the current cycle id, incrementing the cycleInfo[currentCycleID].withdrawRequestsFullFilled and setting the…
Crit/HighSwapping with deadline as block.timestamp and 0 minimum amount out is vulnerable to MEV3S-Clip Finance-H02
Exchange:swap() sets minAmountOut to 0 and in the plugins the deadline is set to block.timestamp.
Crit/HighBatchOut:executeBatchWithdrawFromStrategyWithSwap() gives unfairly different slippage depending on the chosen token3S-Clip Finance-H03
BatchOut:executeBatchWithdrawFromStrategyWithSwap() calls strategyRouter:withdrawFromStrategies() with the token having the most requests shares.
Crit/HighBatchOut:withdrawFulfill() can be DoSed by spamming withdrawal requests, leading to OOG reverts3S-Clip Finance-H04
Anyone can schedule as many withdrawals as they want in BatchOut:scheduleWithdraw(), specifying little shares to different withdrawTo addresses.
Crit/HighScheduled withdrawals with unsupported tokens will be halted3S-Clip Finance-H05
A token might be supported at time A, letting users withdraw in BatchOut using this token.
Crit/HighHalted withdrawals in BatchOut due to setting withdrawTo to address(0) in scheduleWithdrawal()3S-Clip Finance-H06
BatchOut:scheduleWithdraw() allows sending a withdrawTo argument of 0. Some tokens, such as USDT, revert when transferring tokens to address 0.
Crit/HighYield loss due to StrategyRouterLib:rebalanceStrategies() not allocating saturated strategy deposits3S-Clip Finance-H07
StrategyRouterLib:rebalanceStrategies() withdraws excess funds from strategies and adds it up to the router balance.
MediumPotential overflow in PancakeSwapPlugin:getRoutePrice()3S-Clip Finance-M01
Currently the price is calculated as uint256 routePrice = FullMath.mulDiv(uint256(sqrtPriceX96) uint256(sqrtPriceX96), precision0, 2 (96 2));.
MediumDoSed StrategyRouter:withdrawFromStrategies() if strategyTokenBalancesUsd[i] is too small in the swapping phase3S-Clip Finance-M02
StrategyRouter:withdrawFromStrategies() withdraws from the idle strategy of the withdraw token and then tries to withdraw from idle strategies and later strategies with other tokens.
MediumHalted withdrawals in BatchOut:withdrawFulfill() due to tokens transfer() reverting on 0 transfer amount3S-Clip Finance-M03
Some tokens may revert on 0 amount transfers. This means that if someone schedules a withdrawal of 1 share, it could convert to a 0 amount and halt all the other withdrawals.
MediumBatch:withdraw() can be DoSed by frontrunning it with strategyRouter:allocateToStrategies()3S-Clip Finance-M04
Anyone who wants to withdraw from a batch via Batch:withdraw() can be frontrunned by an allocateToStrategies() call, which increases the cycle id, making the withdrawal revert.
MediumInconsistent batch:rebalance() behavior when some strategies reach their limit, leading to yield loss3S-Clip Finance-M05
Strategies are allocated funds pro-rata to their weights. However, when the target of a strategy is reached, it won't be allocated any more funds.
MediumStrategyRouterLib.sol bug when subtracting from balance3S-Clip Finance-M06
Lines 665 and 666 of the StrategyRouterLib have the following code: currentTokenDatas[j].currentBalance -= desiredAllocationUniform; currentTokenDatas[j].currentBalanceUniform -= desiredAllocationUniform;
MediumFee on transfer tokens transfer less tokens than what is stored in the receipt on deposits3S-Clip Finance-M07
Some tokens have a fee on transfer, such as USDT, although it is disabled currently (line 127 and 177).
MediumTokens with callbacks may allow malicious attackers to steal the protocol3S-Clip Finance-M08
In Batch.sol, function withdraw() burns the receiptId after transferring the tokens to the user.
Low/InfoMissing fee refund on Batch.sol3S-Clip Finance-L01
The msg.value sent to Batch:deposit() might be bigger than the depositFeeAmount, which means that the contract will receive more native than the fee.
Low/InfogetDepositFeeInBNB() assumes a stablecoin price of 1 USD, which may not be true if it depegs3S-Clip Finance-L02
In Batch.sol, function getDepositFeeInBNB() calculates the fee amount in BNB, considering that the stablecoin price is 1 USD, which may not be true, as we've seen with USDC in the past.
Crit/HighAaveV3 RewardsController provides rewards in any token, should be handled separately3S-GLACIER-H01
The RewardsController from AaveV3 rewards are separate from the yield accrued and can be several different tokens. Thus, they should be handled differently.
Crit/HightotalReserves should be fetched from the strategies individually and summed up in the ReservePool3S-GLACIER-H02
Currently totalReserves is updated based on deposits and withdrawal amounts to the reservePool. This means that rewards accrued in the strategies will not be accounted for, effectively not earning any yield.
Crit/HighincreaseNetworkTotal() allows big arbitrage opportunities by depositing before an increase transaction3S-GLACIER-H03
Increasing the network total is done via the increaseNetworkTotal() function call. Given that glAVAX allows instant withdrawals, it's very easy for MEV bots to steal rewards from legitimate users.
Crit/High_rebalanceWithdraw() mechanism in glAVAX allows arbitrage opportunities by changing the shares/AVAX ratio3S-GLACIER-H04
The balance available for withdrawals is tracked in address(this).balance. The implementation itself is correct and users will have enough liquidity to withdraw given enough balance is accumulated.
Crit/HighIn glAVAX, function _rebalanceWithdraw() withdraws incorrect amount from WAVAX address3S-GLACIER-H05
In the function rebalanceWithdraw() if it is necessary to withdraw from WAVAX in order to satisfy a withdrawal, the code will compare the balance of the WAVAX contract and how much is needed to satisfy the withdrawal.
MediumStrategy withdraw may fail if weights of strategies differ from the real values and might lead to frozen ReservePool3S-GLACIER-M01
When depositing or withdrawing in the ReservePool, it deposits/withdraws individually from the strategies based on the weights. In the case of withdrawals, the transaction might revert.
MediumWithdraw snapshot logic can be tricked allowing users to withdraw right away3S-GLACIER-M02
The current withdraw snapshot implementation allows users to withdraw a certain amount after other users withdraw this amount.
MediumIn GReservePool, if a strategy is frozen reserve pool stops working3S-GLACIER-M03
If a strategy stops working it will become impossible to use the Reserve Pool. The functions deposit(), withdraw() and withdrawAll() will no longer work.
Low/InfoIn glAVAX, should use .call instead of .transfer3S-GLACIER-L01
The functions withdraw() and claim() make transfers directly to the user using payable(user).transfer(amount). .transfer can only forward 2300 gas which means it can fail for some contracts and stop withdraws.
Low/InfoWhen changing addresses, use 2 step transfer and/or address 0x0 checks3S-GLACIER-L02
When changing important addresses, it's best to use additional safety measures to prevent wrong addresses from being set.
Low/InfoStrategy percentages will differ over time as yield accrued differs in ReservePool3S-GLACIER-L03
The ReservePool always deposits and withdraws according to the specified strategy weights. As the strategies have different yield rates, the actual weights will change over time.
Low/Inforebalance() in glAVAX reverts if currentReserves == reserveTarget3S-GLACIER-L04
rebalance() in glAVAX deposits to the ReservePool if there is available liquidity (balance) and the currentReserves are smaller than the reserveTarget.
Low/Inforeceive() in glAVAX should only allow wAVAX3S-GLACIER-L05
The current implementation of the receive() function in glAVAX allows any smart contract to call it, leading to lost funds. Refactor the function to only allow transfers from wAVAX. Currently being reviewed by the team.
Low/InfoUsage of transferFrom() could revert if used from itself3S-GLACIER-N01
Under normal circumstances the usage of transferFrom() in the function withdraw() in AaveV3Strategy and in GReservePool would revert as it would be necessary for the contract to approve itself.
Low/InfoStrategies in the ReservePool could be implemented as an array and Strategy packed3S-GLACIER-N02
The strategies are stored in the ReservePool as a mapping strategies and length strategyCount. This could be reduced to an array of Strategy, Strategy[] public strategies;, increasing readability and gas savings.
Low/InfoImplement _transferShares() to prevent having to convert shares/wAVAX twice in glAVAX3S-GLACIER-N03
Sometimes the quantity of shares on hand is available instead of the corresponding wAVAX amount.
Low/Inforebalance(), withdrawAmount needed from the ReservePool can be simplified3S-GLACIER-N04
In function rebalance(), the withdrawAmount from the ReservePool can be computed inside the first if statement. Change the code to Currently being reviewed by the team.
Low/InfoChecks effects interactions pattern should always be used3S-GLACIER-N05
The checks-effects-interactions pattern should be used whenever possible, even if apparently it has no consequences. There are some instances specified in the relevant links where it isn't followed.
Low/InfoIf statements can be inverted to increase readability3S-GLACIER-N06
Inverting if statements can help increase readability and decrease overall code complexity. Take a look at the following example.
Low/InfoavaxAmount is never 0 in withdraw() in the first if (avaxAmount > 0 && ...)3S-GLACIER-N07
In function withdraw(), checking if (avaxAmount 0) is not required since it is Remove avaxAmount 0 from the if statement. Currently being reviewed by the team.
Low/InfoWithdraw requests could be stored in a simpler way in glAVAX3S-GLACIER-N08
Global and user withdrawal requests are stored in a mapping and a variable tracking the length respectively.
Low/InfoIn glAVAX, checking that amount > 0 earlier can save some gas3S-GLACIER-N09
In the function withdraw() the function borrowLiquidity() is called to borrow funds from the lending pool if necessary.
Low/InfoIn glAVAX, naming convention for storage variable should be consistent3S-GLACIER-N10
The code follows the convention that storage variables start with an . In glAVAX some variables have the others do not. They should all follow the same rules to increase code readability.
Low/InfoStorage variables should be cached whenever possible to save gas3S-GLACIER-N11
Storage reads should be avoided whenever possible to save gas, which can be achieved by caching the variables in memory.
Low/InfoUnnecessary user balance check in _withdrawRequest()3S-GLACIER-N12
The balance of the user when withdrawing against the amount passed in as argument is already checked in the withdraw() function, there's no need to check again in withdrawRequest().
Low/InfoTypo in fufillWithdrawal() in glAVAX, should be fulfillWithdrawal3S-GLACIER-N13
Typo in fufillWithdrawal(). Currently being reviewed by the team.
Low/InfoIn GReservePool, remove unnecessary logic3S-GLACIER-N14
In the function withdraw(), the variable amountTransfered is going to be equal to the biggest of two variables, either totalWithdraw or amount.
Low/InfoIn GReservePool, use constants naming conventions3S-GLACIER-N15
There are two constants in this contract (maxStrategies and defaultStrategyWeigth), they should use the naming convention of being in capital letter the same way it is done in the other contracts.
Low/InfoIn GLendingPool, event parameters can be indexed3S-GLACIER-N17
In GLendingPool the events Borrowed and Repaid can have the parameter user as indexed. Currently being reviewed by the team.
Low/InfoRemove unused imports3S-GLACIER-N18
If an import is never used it should be removed to save on code size. In wglAVAX imports PausableUpgradeable, IWAVAX, IGReservePool, IGLendingPool, AcessControlManager and GlacierAddressBook are never used.
Crit/HighIn BaseRouter, the beneficiary isn't checked when starting a flashloan action and it replaces the previous beneficiary3S-FUJI-C01
When doing a flashloan action in the BaseRouter.sol the beneficiary isn't compared with the beneficiary of the previous action, and the variable gets replaced with the beneficiary for the flashloan, this can lead to a…
Crit/HighWrong tokensToCheck logic in BaseRouter enables attackers to steal funds3S-FUJI-C02
The BaseRouter implements a mechanism to prevent tokens from being stuck that checks the balance of the router before and after the actions and ensures it remains equal.
Crit/HighChanging providers might lead to lost assets in BorrowingVault and YieldVault3S-FUJI-H01
totalAssets(...) and totalDebt(...) are fetched from the set of providers, which can be changed.
Crit/HighREBALANCER_ROLE can drain funds by rebalancing in a loop in the BorrowingVault3S-FUJI-H02
The rebalancer in the borrowing vault receives a fee for providing debtAsset to repay the originating provider and send to the destination provider.
Crit/HighUniswapV2Swapper uses block.timestamp for deadline [Out of scope]3S-FUJI-H03
Uniswap sets a deadline to limit arbitrage opportunities if the swap does not get included right away. If the swap specifies a deadline of block.timestamp, then the swap transaction can be included in any block.
Crit/HighConnextHandler executeFailedWithUpdatedArgs(...) reentrancy allowedCaller can steal all ConnextHandler tokens3S-FUJI-H04
In executeFailedWithUpdatedArgs(...), the allowedCaller can steal all assets available on the ConnextHandler by calling executeFailedWithUpdatedArgs(...) again after the xBundle(...) call.
Crit/HighWrong transformation in function previewMintDebt(...)3S-FUJI-H05
In the BorrowingVault.sol the function previewMintDebt(...) is supposed to take an amount of shares and turn them into an amount of debt.
Crit/HighsafeApprove(...) reverts if approval is different than 0, use safeIncreaseAllowance(...) instead3S-FUJI-H06
The BaseRouter interacts with the vault and some actions require tokens being pulled from the BaseRouter to the vault, which means that the BaseRouter must first approve the vault.
Crit/HighAttackers can claim deposits to vaults if users specify the router as receiver and don't withdraw shares after3S-FUJI-H07
The BaseRouter checks if the initial balance of the tokens interacted with is the same as the end balance. However, for vaults, it only checks the balance of the underlying asset, not the shares themselves.
MediumBorrowingVault, if the debt/assets ratio falls too much, liquidators could choose to repay debt equal to the assets at a discount3S-FUJI-M01
Currently the borrowing vault enables liquidating a user's position 50% if the debt/assets ratio is above liqRatio, but below liqRatio / FULLLIQUIDATIONTHRESHOLD.
MediumPayback can be DoSed in the BorrowingVault, may be profitable for liquidators3S-FUJI-M02
The borrowing vault enables anyone to payback the debt of another user. Thus, attackers can frontrun a user payback transaction, payback 1 wei and stop users from paying back their debts.
MediumApproval in BaseVault reduces over time3S-FUJI-M03
BaseVault stores users allowances as underlying assets allowances. When a user calls, for example, approve(...), it converts the shares amount in the argument to a corresponding asset amount.
MediumBaseFlasher transfers tokens and then calls xBundle(...), so the sent tokens can't be returned due to the balance check3S-FUJI-M04
The BaseRouter checks if the balances of the assets remain the same after the xBundle(...) execution.
Medium_crossTransfer(...) reverts for smart contracts that don't share the same address on different chains3S-FUJI-M05
crossTransfer(...) has a beneficiary check such that the receiver of the funds in the destination chain must be the same as the previous beneficiary (most likely the msg.sender or the ConnextRouter).
MediumConnextRouter fails to record failed message if gas sent is not enough3S-FUJI-M06
The ConnextRouter places the xBundleConnext(...) call on a try/catch block to prevent failed actions to make the transaction revert and the funds being given to Connext.
MediumIt's impossible to do a depositETH action on xReceive(...) in ConnextRouter3S-FUJI-M07
The ConnextRouter allows users to bridge items from one chain to another and perform arbitrary actions there.
MediumUsers can claim tokens in ConnextRouter by calling xReceive(...) directly3S-FUJI-M08
The ConnextRouter allows anyone to call xReceive(...) and the amount of assets the user provided is the balance of the contract. Thus, users can use this functionality to sweep any existing funds into their wallets.
MediumConnext delegates can perform important actions, make sure smart contract users implement them3S-FUJI-M09
Connext implements a mechanism of adjusting/forcing parameters in the bridged assets which can be triggered by a delegate.
MediumWithdraw and borrow can be DoSed in BaseRouter3S-FUJI-M10
Withdraw and borrow actions in the BaseRouter require the owner to increase the approval allowances.
Low/InfoHandling someone else repaying debt for the BorrowingVault could be done differently3S-FUJI-L01
In convertDebtToShares(...), if totalDebt(...) is 0, it reverts. This means that, if someone decides to repay the full vault debt, this function would always revert.
Low/InfoBaseVault is not fully ERC5143 compliant3S-FUJI-L02
The BaseVault implements ERC5143, an extension of ERC4626. Thus, it should try to match every requirement of the ERC.
Low/InfoIn BaseRouter, _handleSwapAction(...), users shouldn't be allowed to send funds to an allowed flasher3S-FUJI-L03
handleSwapAction(...) allows sending funds to the Flasher. This should be to pay for a taken flashloan.
Low/InfoBaseRouter, _bundleInternal(...) Action.Flashloan does not check if the selector matches xBundle(...)3S-FUJI-L04
Users make flashloans by calling xBundle(...) with a flashloan action.
Low/Info_crossTransfer(...) should revert if users specify routerByDomain[destDomain] as destination3S-FUJI-L05
This scenario only happens if the router in the destDomain has the same address as the router in the destDomain. Else, the beneficiary check will fail.
Low/InfoexecuteFailedWithUpdatedArgs(...) shouldn't be able to change beneficiary3S-FUJI-L06
When the ConnextRouter message fails, it records the message in the ConnextHandler and sends it the funds.
Low/InfoWhen changing addresses, use 2 step transfer and/or contract size and/or address 0x0 checks3S-FUJI-L07
When changing important addresses, it's best to use additional safety measures to prevent wrong addresses from being set. When the address to change is guaranteed to be a smart account, it accepts the new role.
Low/Info_getBeneficiaryFromCalldata(...) in ConnextRouter should not allow the first action to be depositETH(...)3S-FUJI-L08
Currently issue 3S-FUJI-M07 means that the deposit action can't be performed on a cross transfer at all.
Low/InfoBorrowing is not vulnerable to an inflation attack, it's unnecessary to borrow when initializing the vault3S-FUJI-N01
An inflation attack occurs because attackers manipulate the denominator of the assets calculation by increasing totalAssets(...), which when calculating the shares as shares = assets supply / totalAssets(...), would…
Low/InfoYieldVault maxRedeem(...) unnecessarily converts shares to assets and back to shares again3S-FUJI-N02
maxRedeem(...) enables users to know how many shares they can redeem. In the case of the borrowing vault, it depends on the amount of debt a user has taken, which is taken into account in computeFreeAssets(...).
Low/InfoThroughout code base, implement using SafeERC20 for IERC20 for better readability3S-FUJI-N03
When using SafeERC20, it's common to implement with using SafeERC20 for IERC20. Take a look at the openzeppelin code. Addressed here: Fujicracy/fuji-v2648
Low/InfoBaseFlasher does extra abi.encode unnecessarily3S-FUJI-N04
The entrypoint in BaseFlasher is keccak256(abi.encode(data)) Remove abi.encode and set it as keccak256(data) Addressed here: Fujicracy/fuji-v2614
Low/InfoConnextHandler can store the hash of the failed messages instead3S-FUJI-N05
Storing the failed message itself is very expensive. Store the hash and then when calling executeFailedWithUpdatedArgs(...) send the txn as an argument and match it against the stored hash.
Low/InfoWhen recording failed transactions in ConnextHandler, getting the next Nonce involves an unnecessary for loop3S-FUJI-N06
In ConnextHandler.sol, in order to record a failed transaction it is necessary to know the next nonce in the mapping, this involves a for cycle with a storage read that expends gas.
Low/InfoMismatching calldata in _crossTransferWithCalldata(...) and xReceive(...) in ConnextRouter3S-FUJI-N07
Users can bridge assets by inserting an Action.XTransferWithCall action, with calldata specified in the args field.
Low/InfoIn ConnextHandler, executeFailedWithUpdatedArgs(...), the whole tx is updated on storage if the try call succeeds3S-FUJI-N08
In the ConnextHandle, executeFailedWithUpdatedArgs(...), the failed tx executed field is set to true.
Low/InfoWhen transferring tokens, if the amount is 0, the transfer should be skipped3S-FUJI-N09
Before each transfer call, if the amount is 0, it should be skipped. This is also true for SafeERC20.safeTransfer(...) and SafeERC20.safeTransferFrom(...). An example would be Addressed here: Fujicracy/fuji-v2652
Low/InfoxBundle(...) and xReceive(...) should have nonReentrant modifiers3S-FUJI-N10
xBundle(...) and xReceive(...) make several external calls and check the balances before and after these calls. It's safer to include nonReentrant modifiers to prevent reentrancy.
Low/Info_to argument missing 0x0 address check in the ConnextRouter3S-FUJI-N11
The ConnextRouter does not check, in the crossTransfer(...) and crossTransferWithCalldata(...) functions, if the receiver and routerByDomain[destDomain] equal address(0).
Low/Info_checkNoBalanceChange(...) cycle should break in BaseRouter3S-FUJI-N12
checkNoBalanceChange(...) has a for cycle that goes through all tokens.
Low/Info_handleSwapAction(...) creates situations where the arguments receiver and sweeper need to be the same address in BaseRouter3S-FUJI-N13
In handleSwapAction(...) the function checks if both the receiver and sweeper are the beneficiary given certain conditions.
Low/InfoConstants should be placed as constants and not hardcoded for better readability3S-FUJI-N14
Hardcoded values are hard to track and constants should be used instead.
Low/Info_tempTokenToCheck in BaseRouter does not need to be a state variable3S-FUJI-N15
tempTokenToCheck checks that the balance of the ConnextRouter does not change after having received tokens in xReceive(...) .
Low/InfoUseless else statement3S-FUJI-N17
If a call reverts in the first if statement, the following else is not required. Replace with Addressed here: Fujicracy/fuji-v2625 and Fujicracy/fuji-v2622
Low/InfoSaving parameters in memory without using them spends gas3S-FUJI-N18
In BaseVault.sol several functions receive a variable as a parameter then save it in memory and immediately use it to make a call without making any changes.
Low/InfoDon't return the same memory variable if your passing it as argument3S-FUJI-N19
In BaseRouter, bundleInternal(...), the tokensToCheck array is updated in tokensToCheck = addTokenToList(token, tokensToCheck);.
Crit/HighFixed term loans can be deployed with a wrong fee manager and possibly steal all funds.3S-MAPLE-H01
In MapleLoanInitializer, the address of the fee manager is not whitelisted. Exploit scenario ● Borrower agrees with delegate on the terms and creates a loan with the correct fee manager;
Crit/HighPool Delegates can set a really high origination fee and steal all pool funds.3S-MAPLE-H02
The delegate origination fee can be set to any value at any time and anyone can deploy a loan, so the delegate can receive all the funds of a loan.
Crit/HighPool Delegates can steal all the pool's funds by setting a malicious Withdrawal Manager.3S-MAPLE-H03
The current PoolManager contract allows the pool manager to change the withdrawalManager contract without any checks using the setWithdrawalManager() function.
MediumOpen term loans can be created with zero platform service fee if borrowers create them right after a pool has been deployed.3S-MAPLE-M01
The platform service fee is set on loan deployment; however, when a pool is deployed, its platform service fee is 0 (has not been set yet), so a borrower could deploy a loan right after a pool deployment and have 0…
MediumfundLoan() can be DoSed if returnFunds() is called before it.3S-MAPLE-M02
It's possible to stop loans from being funded by calling returnFunds().
MediumA pool delegate should not be allowed to be a borrower3S-MAPLE-M03
A Pool Delegate should not be allowed to be a borrower otherwise they can leave with the funds at any time with no questions asked.
MediumPool Delegates can set an unreasonably large delegate management fee rate at anytime3S-MAPLE-M04
Using the function setDelegateManagementFeeRate() the pool manager can set an unreasonably large delegateManagementFeeRate (all the way up to 100%) at any time, stopping payments from reaching the pool (i.e.
MediumMapleGlobals, activatePoolManager() has no check that the pool manager is actually a valid pool manager.3S-MAPLE-M05
In MapleGlobals, activatePoolManager(), there is no check that the Pool Manager and Pool Delegate are valid, so the governor can mistakenly set malicious addresses as activated.
MediumDoS attack if assets are transferred before a fundLoan() call.3S-MAPLE-M06
In fixed term loans, a DoS attack is possible if assets are transferred into the MapleLoan before the loan is funded.
MediumLiquidation can be finished without calling triggerDefault() (and repossessing the loan) if pool delegate or governor call finishCollateralLiquidation(...) after impairment.3S-MAPLE-M07
The usual flow is to impair a loan and then call triggerDefault() followed by finishCollateralLiquidation();
MediumPool Delegates can accidentally lock out of their funds LPs in the middle of redeeming.3S-MAPLE-M08
Pool delegates are allowed to change the withdrawal manager address at any time using the setWithdrawalManager() function.
MediumMalicious refinancer danger due to fixed term loan upgrades.3S-MAPLE-M09
In the previous maple-core-v2 code, any address could be chosen for the refinancer when proposing new terms.
Low/InfoMapleLoan, proposeNewTerms() could have a check for duplicate selectors.3S-MAPLE-L01
Terms can be proposed with duplicated calls, which could lead to mistakes or phishing attacks. Send the calls ordered by selector and check that the next selector is strictly bigger than the previous.
Low/InfoMapleLoan, skim() has no zero address transfer check.3S-MAPLE-L02
Could lead to loss of funds if someone sends a zero address by mistake. Add a transfer to 0 address check. Addressed in the following PR:
Low/InfoIn MapleLoanInitializer, the Borrower can choose a different fundsAsset than the lender.3S-MAPLE-L03
The code allows for the Borrower to choose a different fundsAsset than the lender.
Low/InfoBorrowers can prevent MapleLoanInitializer from initializing a loan by using a vanity address.3S-MAPLE-L04
By using the fallback function in the MapleLoanInitializer to initialize a loan (both for open term and for fixed term) it allows the borrower to choose a vanity address to end up in any public function of the…
Low/InfoConsider using a time lock on critical permissioned functions.3S-MAPLE-L05
In critical functions that may affect the decision of users to stay invested in the protocol (for example functions that set certain pool terms, fees, upgrades, etc.), there are no timelocks implemented, which will give…
Low/InfoIn MapleGlobals, there is no check in the dataHash argument when unscheduling a call.3S-MAPLE-L06
In MapleGlobals.sol, in unscheduleCall(), the CallUnscheduled event is emitting dataHash (the hash of the callData) which is never checked, and could be different than the one it is unscheduling.
Low/InfoConsider adding a check to ensure that fees, delegate cover and so on have been explicitly set.3S-MAPLE-L07
Some fees or delegate cover information are set in the globals. When fetched, there is no check that the values for a specific pool or loan manager have been set, so the default 0 value is assumed.
Low/InfoIn PoolManager, setOpenToPublic(), there is no way to unset openToPublic.3S-MAPLE-L08
The setOpenToPublic(...) function only enables setting openToPublic to true, so if a delegate sets a permissioned pool to open by mistake, it's impossible to undo it. Add setOpenToPublic as an argument.
Low/InfoThroughout code-base: missing address checks.3S-MAPLE-L09
Whenever addresses change in the code, it's important to be careful about the possibility of setting the wrong address.
Low/InfoMapleLoan variables read from storage more than once.3S-MAPLE-N01
In the MapleLoan contract, the following variables are loaded from storage multiple times: ● acceptNewTerms(): lender is loaded twice from storage. ● acceptNewTerms(): borrower is loaded from storage three times.
Low/InfoMapleLoanStorage storage slot optimization.3S-MAPLE-N02
On the MapleLoanStorage contract, a storage slot can be reduced by packing the last variable: platformServiceFeeRate (a uint64) with one of the addresses. Pack the variables into a single storage slot.
Low/InfoMapleLoan change the order of require to save on gas.3S-MAPLE-N03
On open-term-loan/MapleLoan.sol, line 84 of acceptNewTerms(), there is a require to check that the refinance commitment according to the terms sent by the user is valid and to do so refinanceCommitment is loaded from…
Low/InfoIn PoolManager, requestFunds(...) repeated variable fetching from storage.3S-MAPLE-N04
On line 221 the factory address is assigned from IMapleProxied(msg.sender).factory(). However, on line 225, IMapleProxied(msg.sender).factory() is called again instead of using the already fetched factory.
Low/InfoIn MapleLoanFactory, isLoan has the same functionality of isInstance and thus can be removed.3S-MAPLE-N05
The mapping isLoan is only modified in the respective MapleLoanFactory.sol contract which extends MapleProxyFactory.sol.
Low/InfoLoanManager code optimizations: no need to load payment struct from storage if loan is unimpaired.3S-MAPLE-N06
In the LoanManager contract, two changes can be made to reduce gas usage and improve readability: In function accountForLoanImpairmentRemoval(): loading the payment struct from storage should only be performed after the…
Low/InfoLoanManager code optimizations: redundant impairment functions.3S-MAPLE-N07
The LoanManager contract has the following code: In this code, functions accountForLoanImpairment(loan) and accountForLoanImpairmentAsGovernor(loan) add unneeded redundancy and make the code more cluttered, therefore…
Low/InfoThroughout code base: use Solidity native errors implementation instead of string errors.3S-MAPLE-N08
Solidity allows for their implementation of native custom errors that is more gas-efficient than using a string within the require function to explain to the user where the call reverted (the way it's implemented in the…
Low/Infoopen-term-loan-private: check dateFunded!=0 first to save gas.3S-MAPLE-N09
On open-term-loan in function makePayment(), first the payment breakdown is called and after that the loan is checked to be active. getPaymentBreakdown() does several storage reads.
Low/Infopool-v2-private: cache list length in memory and uncheck i_ to save gas.3S-MAPLE-N10
In the PoolManager contract, function setIsLoanManager(): variable loanManagerList.length could be held in memory preventing multiple storage reads during the for loop.
Low/InfoThroughout code-base: Homogenize how access control is done.3S-MAPLE-N11
There are some discrepancies in the way that checks for access control/requires are done throughout the code base.
Low/InfoMissing documentation for MapleLoan, SetPendingLender and AcceptLender being implemented on MapleLoan but not on LoanManager.3S-MAPLE-N12
Fixed and Open term loans implement SetPendingLender and AcceptLender; however, the corresponding LoanManagers don't.
Low/InfoMultiple documentation fixes throughout the repository.3S-MAPLE-N13
Throughout the repository, a total of 14 comments and 21 wiki errors were reported. Fix the corresponding errors. Addressed in the following PRs:
Low/Infoglobals-v2-private: typo in function name.3S-MAPLE-N14
There is a typo in the name of the function setContactPause(). Change setContactPause() to setContractPause(). Addressed in the following PR: maple-labs/globals-v2-private61
Nothing matches that. Try a shorter word, or clear the severity filter.