Baseline Markets engaged Guardian to review the security of its concentrated liquidity protocol, supporting a baseline value for its YES token. From the 29th of April to the 9th of May, a team of 5 auditors reviewed the source code in scope.
- Published
- Review window
- April 29 to May 9, 2024
- Language
- Solidity
- Chains
- Blast
- Sector
- Token launches
- 2 Critical
- 8 High
- 15 Medium
- 19 Low
- 0 Informational
Scope
Overview
Baseline Markets engaged Guardian to review the security of its concentrated liquidity protocol, supporting a baseline value for its YES token. From the 29th of April to the 9th of May, a team of 5 auditors reviewed the source code in scope.
Findings 44
-
C-01 Critical DoS By Adding Liquidity On Behalf Of BPOOL DoS Partially resolved
Description
The protocol has three main functions, which are
bump,sweepandslide, to maintain liquidity structure among different ranges. Duringbump, all liquidity is removed from floor, anchor and discovery, and the exactly previous amount of liquidity is added back to anchor and discovery.(,, uint128 liquidityA) = BPOOL.removeAllFrom(Range.ANCHOR); (,, uint128 liquidityD) = BPOOL.removeAllFrom(Range.DISCOVERY); // … BPOOL.manageLiquidityFor(Range.ANCHOR, Action.ADD, liquidityA); BPOOL.manageLiquidityFor(Range.DISCOVERY, Action.ADD, liquidityD);Normally, the protocol should never have more liquidity in anchor than discovery. However, anyone can directly mint on behalf of any other user in Uniswap. An attacker can break this invariant by adding liquidity to anchor and bumping right after.
After this, the
sweepwill always revert due to underflow here.slideandbumpwill also always revert regardless of the active price if the attack is done when thecheckpointTickis equal tofloorUpper+tickSpacing, and the protocol’s liquidity structure will be completely stuck.Recommendation
There is no way to prevent attackers minting directly on Uniswap. To prevent this issue, keep track of the protocol owned liquidity as a separate variable and use it to determine liquidity amounts.
Resolution
Baseline Team: The issue was resolved in PR#48.
-
C-02 Critical Floor Inflation Allows Risk Free Shorts Gaming Resolved
Description
In the
slidefunction the amount of liquidity credited to the floor range is dependent on the balance of theBPOOLcontract after allocating the discovery and anchor positions. Users who hold a large amount ofbAssetscan game this behavior for an immediate and significant gain in their bAsset amount. Consider the following scenario:A Whale user starts at price A and sells
bAssetsto move price into the floor at price B. The user then maliciously sends reserve tokens directly to theBPOOLcontract and triggers aslide. During theslidethe reserve tokens sent this way are attributed to the liquidity of the floor. However since price is inside of the floor, thesebAssetsare "leveraged" as for the reserve tokens deployed to the floor position, there are correspondingbAssetswhich are minted to be paired.Now using the increased "leveraged" liquidity in the floor position, the user swaps all of the remaining reserves they had received from the original sell. Only now, due to the increased liquidity of the floor, they only reach C as a final price on their buy.This way the user's average price on the buy is much lower than the average price on their sell, and they realize an immediate arbitrage profit in terms of
bAssets, this guarantees profit on abAssetshort.Recommendation
Burn any assets in the BPOOL prior to removing the three positions from the liquidity pool, this way malicious actors cannot tamper with the resulting liquidity amounts in the floor position and create profitable scenarios.
Resolution
Baseline Team: The issue was resolved in PR#48.
-
H-01 High Anchor Liquidity Is Incorrectly Calculated After Sweep Logical Error Resolved
Description
During
sweep, discovery liquidity is rebalanced into anchor and floor liquidity. Anchor’s tick range is also increased. It was observed that anchor’s liquidity could decrease post-sweep which is undesirable.The issue stems from the increased anchor tick range and calling
BPOOL.manageReservesFor(Range.ANCHOR, Action.ADD, newReservesA);instead ofBPOOL.manageLiquidityFor().There is no guarantee that
newReservesAis sufficient to maintain or increase anchor’s liquidity, given the new tick range. AlthoughsurplusReservesD(surplus reserves in discovery) is added tonewReservesA, this may not be sufficient as surplus could be small or even zero.As a result, every time
sweepis called anchor liquidity could decrease, leading to a very thin anchor range and poor trading conditions where price fluctuates wildly between floor and discovery.Recommendation
Calculate the amount of reserves needed to maintain
anchor.liquiditythen add surplus reserves before re deploying Anchor liquidity.Resolution
Baseline Team: The issue was resolved in PR#55.
-
H-02 High Invalid Leverage Factor Calculation Logical Error Resolved
Description
The leverage factor is calculated as the ratio between the totalCollateralized and the spot supply (externally owned bAssets). Whenever sweep or slide rebalances are executed, the leverage factor will act as a multiplier for the liquidityPremium.
The main issue relies on the calculation of this multiplier. The first part of the formula correctly calculates the ratio:
leverageFactor_ = totalCollateral / (_bAssetsCirculating - totalCollateral);But the value returned by the function is: l
everageFactor_ += 1e18;This means that if the ratio is 5, the leverage factor will be 1e18 + 5 instead of 5e18 or a 5x multiplier. The issue can also be found at InitializeProtocol.sol#L166 as well.
Recommendation
Use the
FixedPointMathLiblib to correctly return the multiplier value:leverageFactor_ = 1e18 + totalCollateral.divWad(_bAssetsCirculating - totalCollateral);Resolution
Baseline Team: The issue was resolved in PR#66.
-
H-03 High Interest Free Borrowing Due To TimeslotLib Gaming Resolved
Description
Users can borrow reserves from
CreditFacilityusingbAssetsas collateral. A small interest will be paid based on the credit amount and the days added. The issue arises when users borrow more reserves with an existing credit account, without adding more days to expiry time.The protocol will try to calculate the
interestOnNewCollateralbased on thedaysRemaining, but this value is 0 as there won't be any days remaining during the last expiry day.Any user borrowing during the last expiry day of the account, and does not add more days to the expiration, will effectively pay no interest for the new credit. Users might take advantage of this issue, by borrowing reserves from the FLOOR and ANCHOR, and repaying them in the same transaction to the FLOOR, and only pay gas fees.
This will cause the capacity to be increased at will, as well as reduce the ANCHOR reserves that support the current price, opening the opportunity for arbitrageurs to extract reserves from the protocol.
Recommendation
Prevent users from borrowing more credit during the last day of expiry if they are not adding more days to account expiration.
Resolution
Baseline Team: The issue was resolved in PR#51.
-
H-04 High Incorrect Calculation of Upper Tick in Anchor Range Logical Error Resolved
Description
The
MarketMakingpolicy deploys liquidity to FLOOR, ANCHOR and DISCOVERY positions when rebalancing and their tick boundaries are calculated based on thegetTickBoundariesfunction. The upper tick of ANCHOR (lower tick ofDISCOVERYis calculated based on_getUpperAnchorTickwhich is suppose to return the next highest initialized tick that is a multiple ofTICK_SPACINGas stated in the docs.Although the
_getUpperAnchorTickworks with positive ticks, it does not return the correct value for negative ticks. This is due to the fact that the function is rounding the current tick down in absolute value, but negative ticks should be rounded up. When the active tick is in the FLOOR range, the ANCHOR range will disappear when the active tick is positive, but it won't when its negative.This also creates an unexpected scenario where the
sweeprebalance operation can be executed when the price is at ANCHOR range. This operation will only check the reserves at ANCHOR, but not the liquidity removed as bAssets, which will be burned.Recommendation
Consider refactoring the formula to return the correct value. A suggested approach is:
function _getUpperAnchorTick() internal view returns (int24 upperAnchorTick_) { if (checkpointTick % TICK_SPACING == 0) return checkpointTick + TICK_SPACING; upperAnchorTick_ = (checkpointTick / TICK_SPACING) * TICK_SPACING; if (checkpointTick > 0) { upperAnchorTick_ += TICK_SPACING; } }Resolution
Baseline Team: The issue was resolved in PR#49.
-
H-05 High Migration Process Can Be DoS’d DoS Resolved
Description
During the migration process, the protocol will:
- Initialize the pool
- Distribute spot tokens and create credits
- Deploy liquidity to the pool
This migration is not atomic and will take some time, which means there will be a lag between the initialization of the Uniswap pool and deploying liquidity to the pool. In this period, an attacker can add liquidity to any tick he wants, perform a swap, and change the active tick. Since the pool is empty, this can be done with only a few
wei.Attacker injects a swap between steps 1 and 3 above but the migration process continues, and there is no check regarding whether the current tick is the same as
INITIAL_ACTIVE_TICK.There are two possible impacts depending on which way the swap is performed by the attacker. 1. If the attacker swaps below the floor tick, the liquidity deployment will be successful, but the ratio between discovery and anchor will be enormous. 2. If the attacker swaps way above, liquidity deployment will fail due to active tick being in the discovery range.
Recommendation
Consider refactoring the migration process such that there is no time window for swaps to occur between the initialization of the pool and deploying liquidity to the pool.
Resolution
Baseline Team: The issue was resolved in PR#68.
-
H-06 High Interest Formula Favours Longer Credits Logical Error Resolved
Description
The protocol has a credit feature and the interests for these credits are paid upfront. Interest is meant to be linear based on
INTEREST_PER_DIEMvariable and credit length.However, interest formula favours longer credits due to an error in it. Currently:
- Yearly credit interest is ~348.6 * daily interest instead of 365.
- Yearly credit interest is ~11.6 * monthly interest instead of 12.
This means ~4.5% discount in interest when taking yearly credits.
The protocol has two major revenue streams: LP fees and credit interests. This income is used to increase the baseline value of the token. Due to this formula favouring longer credits, protocol loses ~3-5% of it’s expected interest revenue (exact number will be affected by average credit length).
Recommendation
Update the interest formula to prevent value loss, and ensure the interest rate is fixed regardless of the credit length.
Resolution
Baseline Team: The issue was resolved in PR#69.
-
H-07 High All Gas Yields Earned On Blast Are Lost Logical Error Resolved
Description
This protocol will be deployed on Blast, which provides a feature to claim gas fees consumed by smart contracts. Contracts can claim 50% to 100% of gas fees depending on the claim rate.
The default gas mode for contracts on Blast is Void, and all consumed gas is sent to the sequencer. For these gas fees to be claimed, smart contracts must be configured by interacting with the BLAST contract at
0x4300000000000000000000000000000000000002address.Recommendation
Configure smart contracts to set gas mode
claimable, and use this additional income to increaseblv.Resolution
Baseline Team: The issue was resolved in commit f1f9e5d.
-
H-08 High Protocol Loses Blast Points And WETH Rebasing Yields Logical Error Resolved
Description
The reserve token in the protocol is Blast’s rebasing WETH token, which has automatic rebasing by default (not like the native ETH with void by default) for both EOAs and smart contracts.
Normally, these reserve tokens are not held in the Baseline contracts. They are deployed into the Thruster pools, which set their WETH yield configuration as claimable in their constructor, as liquidity. So, Thruster pools earn rebasing yields from liquidity providers’ assets, and the factory owner of the pool can claim these yields.
According to Thruster docs, liquidity providers earn Blast points depending on their WETH balances since they help Thruster to earn yields. Thruster says the system is automatic, but there are some checks: “Pools with a significant amount of liquidity providers being contracts (not EOAs) need to be manually verified to ensure that the Points are claimable by the contracts”.
BPOOL contract itself will be the biggest liquidity provider but it has no way to claim Blast points. Therefore, the Thruster protocol will not allocate these earned points to Baseline. ”…it should not be allocated to a contract address that does not support the Blast Points API as those Points then become unclaimable or transferable…”.
This would also be a case if the protocol decides to deploy liquidity in another Uniswap fork.
Recommendation
Use
IERC20Rebasinginterface instead of regularERC20for reserve token, and then configure smart contracts in a way to be integrated with Blast points system.Resolution
Baseline Team: Resolved.
-
M-01 Medium Lacking Initial Tick Validation May Brick The Protocol DoS Resolved
Description
The
BPOOLmodule is initialized with the starting values foractiveTickandfloorTick. The current validation only checks thatactiveTick > floorTick. At this moment,checkpointTickis also initialized with the same value asactiveTick.The issue rises when initial
activeTickvalue is not greater thanfloorTick + TS, as the active tick will be at the FLOOR range, ANCHOR range won't exist, and DISCOVERY range will be initialized with 0 liquidity.Additionally, if this initial setup is created, then no
marketMakingoperations can be executed, even if the price start trading upwards:bump- checkpoint is at the FLOORsweep- Panic reverts when calculatingliquidityPremiumasliquidityAis 0slide- activeTick is above checkpoint
Recommendation
Replace the tick values validation in the
initializePoolwith:require(_initialFloorTick + TS < _initialActiveTick, "Invalid tick values");Resolution
Baseline Team: The issue was resolved in PR#67.
-
M-03 Medium Anchor Liquidity Heavily Reduced Logical Error Resolved
Description
The
slideoperation scales down the ANCHOR liquidity using theinverseLiquidityPremiumas follows:liquidityA = uint128(uint256(liquidityA).divWad((inverseLiquidityPremium)));The issue arises when all (or most) of the circulating supply is collateralized, and the leverage factor is a very large number. This means the
liquidityAcan drop to unexpected low values, creating high slippage to the users currently trading.If slide is triggered when the price is at the ANCHOR range (normal scenario), the operation will effectively remove most of the liquidity from the active trading range, moving the active reserves to the FLOOR range. Although he capacity of the system is still increasing, any subsequent sales will move the active tick into the FLOOR, something the protocol wants to avoid.
In order to restore the correct liquidity of the ANCHOR, price will need to trade up into DISCOVERY and receive surplus reserves. Although this might temporarily restore the liquidity, the
slideoperation will keep scaling down the ANCHOR every time its triggered, as long asliquidityA >liquidityThreshold.Recommendation
Consider adding a max value to the leverage factor, limiting the reduction of the ANCHOR liquidity. Alternatively, consider using the following implementation for the
getLeverageFactorfunction:leverageFactor_ = 1e18 + totalCollateral.mulWad(leverageRange).divWad(_bAssetsCirculating);
Where
leverageRangeis a new variable configurable by the protocol. Otherwise consider implementing another implementation taking points 1 & 2 into account.Resolution
Baseline Team: The issue was resolved in PR#65.
-
M-04 Medium Liquidity Premium Rounds Down To 0 Logical Error Resolved
Description
The liquidity premium is used to calculate the liquidity of the
DISCOVERYrange, based on theANCHORrange liquidity, during rebalances.The main goal is to scale
DISCOVERYrange liquidity based on the ratio depending on the tick distance of the active tick to the floor tick, and theTICK_PREMIUM_FACTOR.The issue involves the initialized value of
TICK_PREMIUM_FACTOR. There is a discrepancy between the tests and the deploy script, as the tests use4800but the deploy script uses4800e18. In case the4800e18value is used, it will cause thegetLiquidityPremiumcalculation round to 0, whenever the tick difference is less than 4800. During asweepandslidethis issue will makeDISCOVERYliquidity equal to theANCHORliquidity.Recommendation
Be sure
TICK_PREMIUM_FACTORis initialized with the correct value of4800.Resolution
Baseline Team: The issue was resolved in PR#51.
-
M-05 Medium Discovery Liquidity Increased Above MAX_DISCOVERY_RATIO Logical Error Resolved
Description
The ratio between the DISCOVERY and ANCHOR liquidity during a sweep operation should be capped by the MAX_DISCOVERY_RATIO (5x), following this logic:
liquidityPremium = (liquidityPremium / liquidityA) > MAX_DISCOVERY_RATIO ? liquidityA *MAX_DISCOVERY_RATIO : liquidityPremium;The issue relies on the ratio
liquidityPremium / liquidityA, as this division rounds down in Solidity. Therefore, every time this division rounds down to the value of MAX_DISCOVERY_RATIO (5), thenliquidityPremiumvalue is not updated, due to the>comparison.Because
liquidityPremiumon its own is larger thanliquidityA * MAX_DISCOVERY_RATIO, this rounding issue allows the DISCOVERY liquidity to increase up to almost 7x the ANCHOR liquidity during sweep rebalancing.Recommendation
Consider updating the comparison to include ratios equal to MAX_DISCOVERY_RATIO: liquidityPremium = (liquidityPremium / liquidityA) >= MAX_DISCOVERY_RATIO ? liquidityA *
MAX_DISCOVERY_RATIO : liquidityPremium;This preventsliquidityPremium / liquidityAratio to end up being greater than 6x.Resolution
Baseline Team: The issue was resolved in PR#59.
-
M-06 Medium Bump Reverts Due To Lack Of Reserves DoS Acknowledged
Description
An edge case appears when the price is at DISCOVERY range, and all reserves from FLOOR and ANCHOR are borrowed. This could happen when there is heavy buying, and all bAssets are collateralized. If we try to trigger a
bumpoperation, this will revert at this function:BPOOL.manageLiquidityFor(Range.DISCOVERY, Action.ADD, liquidityD);The issue is that when we try to mint liquidity in the Uniswap Pool, the calculation for the token amounts owed to the pool are rounded up:
getAmount1Delta(sqrtRatioAX96, sqrtRatioBX96,uint128(liquidity), true).Due to the rounding up, more reserves are requested to be transferred in the
uniswapV3MintCallbackthan available, preventing the bump operation from occurring.Recommendation
Prevent the DoS by ensuring that no more reserves are requested for the DISCOVERY range than available:
uint256 reservesNeeded = LiquidityAmounts.getAmount1ForLiquidity( discovery.sqrtPriceL, sqrtPriceA > discovery.sqrtPriceU ? discovery.sqrtPriceU : sqrtPriceA, liquidityD ); uint256 reserveBal = BPOOL.reserve().balanceOf(address(BPOOL); if (activeTick > discL && liquidityF == 0) BPOOL.manageReservesFor(Range.DISCOVERY, Action.ADD, reservesNeeded > reserveBal ? reserveBal : reservesNeeded); else BPOOL.manageLiquidityFor(Range.DISCOVERY, Action.ADD, liquidityD);Resolution
Baseline Team: Acknowledged.
-
M-07 Medium Rebalance Threshold May Prevent Sliding Operation DoS Acknowledged
Description
The
REBALANCE_THRESHOLDis a param set in the constructor ofMarketMakingpolicy. This means that this param can be configured by the protocol.Although the current deployment script sets the initial value to 1, any value above this will cause unexpected behavior of the
slideoperation. This is due to the fact that if checkpoint and active ticks are less than 2 TS away from the floor tick,slidecan't be triggered if the price trades into the FLOOR range (canSlidetick will be located below the floor tick).Therefore, the protocol won't be able to correctly rebalance the liquidity when there is heavy selling of bAssets.
Recommendation
Remove the
REBALANCE_THRESHOLDfrom the constructor params and set it as a constant inside theMarketMakingpolicy, with value 1.Resolution
Baseline Team: Acknowledged.
-
M-08 Medium Incorrect Circulating Supply Calculation Logical Error Acknowledged
Description
The
getCirculatingSupplyfunction removes the current liquidity inbAssetsfrom the total token supply. The goal is to determine how manybAssetscan be sold into the pool.There are cases when this function returns an outdated value, due to the fact
bAssetfees are not accounted for in the calculation. Consequently, the value returned might be slightly above the real value.During heavy selling of
bAssetsinto the pool, the difference can increase as there will be more pending fees to claim. This can impact the off-chain systems for managing the operations, as the function will not return the correct state. Even if the system appears solvent using this getter function, a market making operation may revert as the real circulating supply is recalculated during the execution.Recommendation
Consider adding the pending fees in bAssets to the
getCirculatingSupplycalculation.Resolution
Baseline Team: Acknowledged.
-
M-09 Medium Initial Deployed Liquidity Missing Crucial Validations Validation Resolved
Description
For the V2 migration, the
InitializeProtocolpolicy will be used. This will initialize the pool, mint the initial spot supply, setup credits and deploy the pool liquidity.The issue relies on the
deployLiquidityfunction, as it only verifies the new deployed capacity is above the initial circulating supply. The missing validations are:- Verify the new
liquidityDis not aboveMAX_DISCOVERY_RATIO, as the tick premium and leverage
can be high.
- Validate the spot supply is already minted, as the system might be insolvent and
distributeSpot
does not check this.
- Validate credit is already setup, similar to spot, circulating supply will be minted and can make the
system insolvent.
Although there is some documentation about the steps for the V2 migration, the code does not enforce these steps, so there could be arbitrage attacks that can be triggered if the liquidity is not setup correctly.
Recommendation
Check if the liquidity structure does not allow
bumpto be executed right away. Additionally, consider adding checks to ensure no more circulating supply is minted after thedeployLiquidityis executed.Resolution
Baseline Team: Resolved.
- Verify the new
-
M-10 Medium Virtual Liquidity Lower Than Expected Logical Error Resolved
Description
When the
slideoperation is triggered, thevirtualLiquidityFis calculated based on the total collateral and the reserves removed from the FLOOR, using the lower and uppersqrtPriceof the entire range.In case the
slideis executed when the active tick is in the FLOOR range,virtualLiquidityFwill still use the uppersqrtPriceof the range, instead of the current price. This issue will cause thevirtualLiquidityFto appear smaller than expected.Recommendation
Consider using the correct limit price in the formula:
(uint160 sqrtPriceA,,,,,,) = BPOOL.pool().slot0(); uint256 virtualLiquidityF = uint256( LiquidityAmounts.getLiquidityForAmount1( floor.sqrtPriceL, sqrtPriceA < floor.sqrtPriceU ? sqrtPriceA : floor.sqrtPriceU, CREDT.totalCreditIssued() + reservesF ) );Resolution
Baseline Team: Resolved.
-
M-11 Medium Operations Using Outdated Circulating Supply Logical Error Resolved
Description
When a credit is defaulted, its collateral is sent directly to the
BPOOLcontract through the_burnDefaultedCollateralfunction. This creates a temporary condition where the circulating supply is higher than expected, if the market making operations are executed before defaulting the expired loans.In the
bumpoperation, an increased circulating supply will effectively mint more tokens than expected for the inflation basis.Recommendation
Ensure
defaultOutstandingis executed at the start of market making operations.Resolution
Baseline Team: The issue was resolved in commit f8be1bb.
-
M-12 Medium Arbitrage Attack After Slide Operation Logical Error Acknowledged
Description
The market making
slideoperation rebalances the liquidity structure when active tick goes below a certain threshold.In case the heavy selling pushes the price to the FLOOR, the
slideoperation will be the only range with reserves. When liquidity is added to the FLOOR,bAssetswill be minted in this range. This scenario allows arbitrager to buy tokens at discount, trigger sweep/bump operations, and profit from it.Recommendation
Verify the initial liquidity structure does not allow these arbitrage attacks to take place.
Resolution
Baseline Team: Acknowledged.
-
M-13 Medium Attackers Can Borrow With Zero Interest Gaming Resolved
Description
Users can borrow reserve tokens by adding bAssets as collateral to the system and interests for these credits are paid upfront based on a daily rate and the baseline value.
Credits are created based on the baseline value of the bAssets, and interests are also paid based on the baseline value when the credit is created. However, this baseline value only increases in time, which means that more reserves can be borrowed with the same collateral when the baseline value increases.
And lastly, the protocol has a check to ensure the borrow is legitimate (either extension of existing borrow or new borrow). But, this check is incorrect and a user can still borrow without extending and without increasing the collateral.
Attackers can combine all of these to borrow zero interest credits:
- Attacker buys bAssets or already holds.
- Creates a very long term credit when the blv is as low as possible. The interest is paid at this step.
- Waits for blv to increase.
- Borrows again without increasing the collateral and without extending expiry.
- This second borrow will transfer more reserves due to increased blv, but the interest will be 0.
- Attacker can do it repetitively every time the blv increases.
The attacker basically setup his future credits, and paid the interest for them at a very low price, making them effectively free.
Recommendation
Do not allow credits that are not legitimate. Also, charge interest based on the total extra credit instead of charging based on newly added collateral to prevent lost interest in case of
blvincrease.Resolution
Baseline Team: The issue was resolved in PR#58.
-
M-14 Medium liquidityPremium Can Be A Discount Logical Error Resolved
Description
The liquidity premium calculated in function
getLiquidityPremium()is calculated as follows:liquidityPremium_ = uint256(uint24(activeTick -BPOOL.floorTick())).divWad(TICK_PREMIUM_FACTOR).If the
activeTickis less thanTICK_PREMIUM_FACTORaway from the floor tick, then the function returns a proportion less than 100%. Consequently, when the returned proportion is multiplied by the target anchor liquidity in functionssweep()andslide(), the liquidity amount is decreased rather than increased. This results in unexpected liquidity structures when the active tick is less than 4800 ticks from the floor tick.For example, the anchor range liquidity is treated as the "top of book" liquidity, and is targeted to have a basis of 1/1000th of the floor liquidity.
However consider a liquidity structure where multiple slides have taken place, and the active tick is 2400 ticks above the floor tick. Now the
liquidityPremiumis 2400 / 4800 = 0.5, resulting in a targeted anchor liquidity of 1/2000th.The targeted anchor liquidity in this case is less than the configured 1/1000th base ratio, and as the active tick moves closer to the floor tick the targeted anchor liquidity becomes even smaller, which ultimately creates much less price stability when the anchor range is below 4600 ticks in width. Additionally, liquidity is increasingly allocated away from where trading action will accumulate fees when the anchor is collapsing below a width of 4600 ticks.
Recommendation
When computing the target liquidity threshold for the anchor position, do not allow the anchor position liquidity to drop below the configured 1/1,000th ratio of the floor position liquidity by treating the
liquidityPremiumas a true premium.uint128 liquidityThreshold = uint128(virtualLiquidityF .mulWad(1e18 + getLiquidityPremium()) .divWad(ANCHOR_LIQ_THRESHOLD));Resolution
Baseline Team: Resolved.
-
M-15 Medium getLeverageFactor DoS DoS Resolved
Description
In the
getLeverageFactorfunction the resultingleverageFactoris computed by dividing thetotalCollateralby the_bAssetsCirculating - totalCollateral.However it is possible for the
totalCollateralto be the entirety of the circulating supply in the event that every holder borrows against their bAssets. In this scenario thegetLeverageFactorwill revert with a divide by 0 panic. This results in a DoS for any call to the sweep or slide functions.Recommendation
Add a case to handle the scenario where all circulating assets are being used as collateral in the
getLeverageFactorfunction.If (_bAssetsCirculating - totalCollateral == 0) return maxLeverageFactorResolution
Baseline Team: The issue was resolved in PR#65.
-
M-16 Medium Risk Free Arbitrage Attack DoS Resolved
Description
The main market making operations allow the protocol to rebalance the liquidity and
bumpthe floor price, as long as solvency is maintained.Due to the fact that these operations can be executed in the same transaction, there is an arbitrage attack opportunity that will drain most of the initial reserves from the protocol, without any risk.
Consider the following attack flow: 1. buy
bAssets, pushing the price deep intoDISCOVERY2. trigger asweepto distribute the surplus reserves into theANCHORandFLOORranges, and update checkpoint tick 3. callbumpmultiple times to catch up with the active tick. 4. sell allbAssetsat a profit, removing a huge chunk of reserves liquidity.Recommendation
Limit the amount of times the liquidity operations can be called within a block, or limit it to once every few blocks.
Resolution
Baseline Team: Resolved.
-
L-01 Low Slide Prevents Price To Exit The Floor Logical Error Acknowledged
Description
Fees are accumulated in reserves and
bAssetsin all ranges, and claimed by the market making operations when they are executed. WhilebAssetfees are burned, the reserve fees are added to eitherANCHORorFLOORranges.Slide operation triggered when the active tick is near the floor tick will claim these reserve fees, but
FLOORis the only range that receives them.The issue arises when the fees accumulated are significant, compared to the floor reserves. The
manageReservesForfunction will add liquidity using the balance of the reserves in theBPOOL, and consequently mint morebAssetsthan expected. This will cause the price to have a hard time exiting the floor, as more reserves will be needed on the way up.As the floor will end up with higher liquidity than expected, buyers will have less slippage buying
bAssetsat low prices, opening the possibility of arbitrage attacks, as the capacity will increase due to all the reserves deposited into theFLOOR.Recommendation
Consider updating the logic on how
bAssetsare minted on theFLOORrange when the slide operation is triggered, so the price can exit the range as soon as possible.Resolution
Baseline Team: Acknowledged.
-
L-02 Low Discovery Liquidity Increased After Sliding Logical Error Acknowledged
Description
The
slidemarket making operation is triggered when the active tick is below a certain distance from the checkpoint tick. This rebalances the liquidity on the 3 ranges. TheDISCOVERYliquidity is scaled down during this operation, in favour of increasing the liquidity of lower ranges, increasing the capacity of the system.After sliding, the
DISCOVERYrange will be readjusted at slightly above the active tick. Therefore, in order to allow price to freely trade up, liquidity in this range should always decrease.If the system is highly leveraged through heavy borrowing to the point it starts using
ANCHORliquidity, thenliquidityA < liquidityThreshold, and this may cause theDISCOVERYliquidity to increase after sliding instead.Recommendation
The sweep function should store the liquidity of the discovery range when calling
manageReservesFor(), and cap the new calculated value based on this amount.Resolution
Baseline Team: Acknowledged.
-
L-03 Low Slide May Result In Equal Anchor And Discovery Liquidity Logical Error Acknowledged
Description
During slide, if
liquidityAis equal toliquidityThresholdthen both anchor and discovery will be re-deployed with the same liquidity.This occurs because anchor's liquidity is only reduced by the inverse premium when it is less than
liquidityThreshold.As a result of this edge case, the liquidity structure will deviate from the ideal structure where discovery has more liquidity than anchor.
Recommendation
Reduce anchor's liquidity by the inverse liquidity premium if
liquidityA >= liquidityThreshold. Alternatively, consider increasing discovery's liquidity in this scenario.Resolution
Baseline Team: Acknowledged.
-
L-04 Low Strict Inequality For Anchor Threshold Comparison Logical Error Partially resolved
Description
In the
sweepfunction thesurplusReservesDare allocated to the anchor and floor positions depending on if it would put the anchor above the desired threshold.However if the
reservesA + surplusReservesDwould put the anchor position at exactly the desired liquidity threshold, then the surplus is instead split between the anchor in the floor.In this case it would be more preferable to allocate the surplus to the anchor, as it would put the anchor at exactly the desired liquidity threshold.
Recommendation
Change the
reservesA + surplusReservesD < thresholdReservesAcomparison toreservesA +surplusReservesD <= thresholdReservesA.Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-05 Low Reserve Token Should Use safeTransfer Logical Error Resolved
Description
The reserve token can be any arbitrary token in future pairs, in order to maintain compatibility with as many tokens as possible,
safeTransferandsafeTransferFromought to be used to validate returned values.Recommendation
Use
safeTransferandsafeTransferFromwhen transferring the reserve token throughout the codebase.Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-06 Low Inconsistent Default Pattern Array Lengths Logical Error Resolved
Description
In the
configureDependenciesfunction the dependencies array is initialized with a length of 3, however only 2 entries are written to.Additionally in the
requestPermissionsfunction the zeroth entry for the requests array is not assigned to.Recommendation
Reduce the size of the dependencies array in the
configureDependenciesfunction as well as the requests array in therequestPermissionsfunction.Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-07 Low Superfluous brs Address Superfluous Code Resolved
Description
In the
BPOOLcontract there is an address variable namedbrswhich is not referenced nor assigned to.Recommendation
Remove the
brsaddress state variable.Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-08 Low Users May Repay 0 Amount Validation Resolved
Description
In the
repayfunction there is nothing preventing a user from repaying with a_reservesInvalue of 0. This allows for an unexpected action to be taken on the system and will restful in an erranttransferFromcall with a value of 0 in theupdateCreditAccountfunction.Though nothing currently comes of this, the protocol ought to limit unexpected user flows to reduce attack surface.
Recommendation
Consider validating that the
_reservesInvalue is nonzero in the repay function.Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-09 Low Invalid Event Emission Data Logical Error Resolved
Description
In the
defaultOutstandingfunction theDefaultedevent is emitted with the wrong order of arguments. The correct order for theDefaultedevent is thetimeslot,credit, followed by thecollateral. However the event emission in thewhileloop emts with arguments of thetimeslot,collateral, followed by thecredit.Recommendation
Swap the
collateralandcreditparameters in theDefaultedevent.Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-10 Low Debug Code In Production Superfluous Code Partially resolved
Description
The code includes
console2logs and imports, which are useful for testing, but are completely useless after deployment. These logs will also consume gas when functions are executed.Recommendation
Remove any debug imports, logs or
TODOcomments from the code.Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-11 Low Misleading Comments Documentation Resolved
Description
Some of the comments found in the contracts are either misleading or incorrect, such as:
- BPOOL.V1#L291: The result is based on the checkpointTick instead of the activeTick
- MarketMaking.sol#L152: Incorrect, the activeTick has to be above one tick space from the
checkpoint
- MarketMaking.sol#L219: Incorrect, it's 100% of the surplus instead of 90%
- MarketMaking.sol#L44, MarketMaking.sol#L145 and MarketMaking.sol#L164: Old v1 shift function
mention.
- MarketMaking.sol#L252: Old v1 bin function mention.
Notice that the lines were taken from the
baseline-team-1-pocs repoRecommendation
Remove or update the previous comments.
Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-12 Low Unused Parameters Superfluous Code Partially resolved
Description
The contracts include a handful of unused errors, events, variables and imported contracts. These are:
- BPOOL.v1.sol#L71
- BPOOL.v1.sol#L75
- BPOOL.v1.sol#L89
- CreditFacility.sol#L15
- CreditFacility.sol#L17
- CreditFacility.sol#L38
- CreditFacility.sol#L42
- CreditFacility.sol#L47
- CreditFacility.sol#L48
- InitializeProtocol.sol#L41
- MarketMaking.sol#L12
- CREDT.v1.sol#L226: The event is used but the order of the parameters are incorrect
Notice that the lines were taken from the
baseline-team-1-pocs repoRecommendation
Implement or remove them accordingly.
Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-13 Low Redundant Ternary Operators Superfluous Code Partially resolved
Description
Some of the ternary operators in the contracts are executing redundant operations and should be removed. These include:
- CreditFacility.sol#L141: The prior if condition will prevent an underflow.
- MarketMaking.sol#L200: Code will return early if the current price is lower than the upper tick of the
floor.
Recommendation
Remove them and simply assign the value directly into the variable.
Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-14 Low Defaulted Event Logging Incorrect Data Superfluous Code Resolved
Description
The
Defaultedevent in theCREDTmodule is defined as:event Defaulted(uint256 timeslot_, uint256 credit_, uint256 collateral_);The issue is that when the event is emitted, its using the collateral as the second parameter and credit as the third.
Recommendation
Update the event to emit the correct data:
emit Defaulted(timeslotIter, defaultable.credit, defaultable.collateral);Resolution
Baseline Team: The issue was resolved in PR#50.
-
L-15 Low Unnecessary Allowances Superfluous Code Resolved
Description
Infinite allowances are given to the Uniswap pool in the
initializePoolfunction of theBPOOLmodule. Uniswap pool never uses these allowances and they should be removed.Another point to mention in here is that
msg.senderof the secondapprovecall above is not theBPOOLcontract but theInitializeProtocolcontract. Reserve token allowances are given by the BPOOL contract itself, but the bAsset allowances are given by theInitializeProtocolcontract.Recommendation
Remove unnecessary allowances.
Resolution
Baseline Team: The issue was resolved in PR#63.
-
L-16 Low Array Lengths Don’t Match During Default Configuration Logical Error Resolved
Description
Protocol uses default framework for smart contracts, which are needed to be configured as modules or policies. Incorrect array sizes are used during the configuration of the
MarketMakingcontract.configureDependenciesfunction has a dependency array with a length of 3 while the actual dependency count is 2.requestPermissionsfunction has a permissions array with a length of 8 while the actual permissions count is 7.Recommendation
Use the exact array lengths.
Resolution
Baseline Team: Resolved.
-
L-17 Low Hardcoded Reserve Token Address Is Incorrect Logical Error Resolved
Description
Reserve token address is hardcoded as
address(0x0)in theInitializeProtocolcontract. However, in the Blast ecosystem, reserve token address is0x4300000000000000000000000000000000000004.The deployment script uses the correct address. However, the public variable in the contract is incorrectly defined and it is not used.
Recommendation
Remove the unused variable from the contract.
Resolution
Baseline Team: Resolved.
-
L-18 Low totalCreditIssued Includes Interest Which Has Yet To Be Paid Logical Error Resolved
Description
When a user borrows, both principal (loan amount) and interest are added to the user's account and to the global variable
totalCreditIssued. However, interest is only to be paid afterrepay, so it should not be treated as credit at time of borrow.By including interest in
totalCreditIssued,calculateTotalCapacityis optimistically inflated before the interest is actually paid and added as liquidity. For accuracy and a more conservative approach, interest should be excluded fromtotalCreditIssued.However, there were no risks identified in including the interest since the collateral for the loan is held by the protocol (i.e., it cannot be sold). Even if the loan defaults, burning the loan’s collateral improves overall solvency.
Recommendation
Clearly document this behavior to users.
Resolution
Baseline Team: Resolved.
-
L-19 Low Floor Tick Increment Prevents Sliding Operations Validation Resolved
Description
The
bumpoperation inMarketMakingcontract increments thefloorTickas long as the new floor does not surpass the active and checkpoint ticks. This operation will revert if:activeTick < floorTick + (TICK_SPACING * 2)checkpointTick < floorTick + (TICK_SPACING * 2)
The issue is that these validations allow the
floorTickto end up exactly one TS below thecheckpointandactiveTick. Consider this scenario:- floor tick is at 200
- Price trades higher, ends up in tick 600 exactly.
sweeprebalances liquidity and sets checkpoint to 600bumpis executed and increments floor tick to 400 (still below active and checkpoint)
This scenario will prevent
slideoperations to be executed even if price drops into FLOOR range, as the active tick will need to go below the floor tick to pass the validation:activeTick < (BPOOL.checkpointTick() - (TS * int24(int8(REBALANCE_THRESHOLD)))Recommendation
Consider updating the
incrementFloorTickvalidations, and use<=instead of<for the comparison against thefloorTickvalue.Resolution
Baseline Team: Resolved.
No findings match.
Invariants 125
The review's fuzzing suite asserted 125 invariants. 118 held and 7 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
BUMP-01 | bump() should return true if successful | Held |
BUMP-02 | Floor lower tick should increase by TICK_SPACING | Held |
BUMP-03 | Anchor lower tick should increase by TICK_SPACING | Held |
BUMP-04 | Anchor upper tick should not change | Held |
BUMP-05 | Discovery lower tick should not change | Held |
BUMP-06 | Discovery upper tick should not change | Held |
BUMP-07 | Balance of EMISSIONS_RECIPIENT should increase if there is circulating supply | Held |
BUMP-08 | Anchor liquidity should not change | Held |
BUMP-09 | Discovery liquidity should not change | Held |
BUMP-10 | Floor position reserve balance should not decrease | Held |
BUMP-11 | Anchor position reserve balance should not increase | Held |
BUMP-12 | System is solvent before bump call | Held |
BUMP-13 | System is solvent after bump call | Held |
BUMP-14 | BPOOL should not increase in bAssets | Held |
BUMP-15 | BPOOL should de cleared of reserves | Held |
BUMP-16 | Bump should not revert besides insolvency and failed canBump | Broken |
SWEEP-01 | Should make no state changes if it returns false | Held |
SWEEP-02 | Sweep is callable when active tick moves up by more than one TICK_SPACING and is less than DISCOVERY upper tick (within Baseline | Held |
SWEEP-03 | Positions) Active tick should be in anchor range after sweep rebalance operation | Held |
SWEEP-04 | Discovery lower tick should be greater than activeTick after sweep | Held |
SWEEP-05 | Active Tick Matches Checkpoint Tick | Held |
SWEEP-06 | Active Tick Is Above Floor | Held |
SWEEP-07 | Anchor range should not shrink | Held |
SWEEP-08 | Anchor liquidity should not decrease | Broken |
SWEEP-09 | Floor liquidity should not decrease | Held |
SWEEP-10 | Discovery liquidity should not exceed max ratio | Broken |
SWEEP-11 | BPOOL bAsset balance is zero | Held |
SWEEP-12 | BPOOL reserve balance is zero | Held |
SWEEP-13 | After sweep no bAssets in floor | Held |
SWEEP-14 | After sweep no reserves in discovery | Held |
SWEEP-15 | Anchor reserves increased | Held |
SWEEP-16 | Anchor reserves non-zero | Held |
SWEEP-17 | Anchor bAssets non-zero | Held |
SWEEP-18 | Discovery bAssets non-zero | Held |
SWEEP-19 | Floor Reserves increased | Held |
SWEEP-20 | Solvency Is Maintained Before Sweep | Held |
SWEEP-21 | Solvency Is Maintained After Sweep | Held |
SWEEP-22 | Capacity is greater after sweep | Broken |
SWEEP-23 | Sweep should not revert beyond Insolvent error | Broken |
SLIDE-01 | Should make no state changes if it returns false | Held |
SLIDE-02 | Slide is callable when active tick moves down by more than one TICK_SPACING from checkpoint tick and is less than DISCOVERY lower tick (You | Held |
SLIDE-03 | can't slide in DISCOVERY range) Anchor should shrink on slide | Held |
SLIDE-04 | Discovery range size should remain the same | Held |
SLIDE-05 | Checkpoint tick updated to active tick | Held |
SLIDE-06 | Floor ticks should not change | Held |
SLIDE-07 | Discovery Liquidity Should Decrease | Broken |
SLIDE-08 | Anchor liquidity should not increase | Held |
SLIDE-09 | Floor reserves should not decrease by a significant amount | Held |
SLIDE-10 | BPOOL bAsset balance should be zero | Held |
SLIDE-11 | BPOOL reserve balance should be zero | Held |
SLIDE-12 | Anchor reserves should not increases | Held |
SLIDE-13 | Discovery bAssets are non-zero | Held |
SLIDE-14 | Floor reserves are non-zero | Held |
SLIDE-15 | Solvency Is Maintained Before Slide | Held |
SLIDE-16 | Solvency Is Maintained After Slide | Held |
SLIDE-17 | Capacity should not decrease | Held |
SLIDE-18 | Circulating supply should not increase on slide by a significant amount | Held |
SLIDE-19 | Slide should not revert besides Insolvent error | Held |
BORROW-01 | Days added or account expiry must be greater than zero for a successful borrow transaction | Held |
BORROW-02 | New expiry return data after borrow is called should match the creditor's account details | Held |
BORROW-03 | The expiry must be in the future | Held |
BORROW-04 | Borrowers reserve balance should increase by the borrow reserveOut amount | Held |
BORROW-05 | Borrowers bAsset balance should decrease by the borrow collateral amount | Held |
BORROW-06 | Borrowers account collateral should increase by deposited collateral | Held |
BORROW-07 | Borrowers account credit should increase by the sum of received reserves and interest paid | Held |
BORROW-08 | User should always pay interest for credits | Broken |
BORROW-09 | Credit can't be higher than collateral, before borrow operation | Held |
BORROW-10 | Credit can't be higher than collateral, after borrow operation | Held |
BORROW-11 | The totalInterestAccumulated should increase by the interest paid after every borrow | Held |
BORROW-12 | CreditFacility bAsset balance should be 0 | Held |
BORROW-13 | CreditFacility reserve balance should be 0 | Held |
BORROW-14 | BPOOL should hold no reserve in its balance | Held |
BORROW-15 | If the borrowed credit is paid with reserves in floor position, reserves in anchor should not | Held |
BORROW-16 | change If the borrowed credit is paid with reserves in floor position, reserves in discovery positions | Held |
BORROW-17 | should not change If the borrowed credit is paid with reserves in floor and anchor positions, reserves in | Held |
BORROW-18 | discovery positions should not change Borrow should not revert if the function parameters are valid and there's enough reserve | Held |
REPAY-01 | to cover the credit The amount of bAsset returned to user should be equal to the amount of collateral in the | Held |
REPAY-02 | user’s credit account The amount of reserves returned by user should be equal to the amount of credit in the user’s | Held |
REPAY-03 | credit account There should be no reserve stuck in creditFacility after repay operation | Held |
REPAY-04 | There should be no reserve stuck in BPOOL after repay operation | Held |
REPAY-05 | Credit can't be higher than collateral, before repay operation | Held |
REPAY-06 | Credit can't be higher than collateral, after repay operation | Held |
REPAY-07 | The repaid reserves should go into floor position | Held |
REPAY-08 | If the REPAY-ed credit is paid with reserves in floor position, reserves in anchor should not change | Held |
REPAY-09 | If the REPAY-ed credit is paid with reserves in floor position, reserves in discovery should not change | Held |
REPAY-10 | Repay should not revert given if user’s account credit is greater than zero, and can transfer the | Held |
DFLOT-01 | required reserve amount Total collateralized should decrease | Held |
DFLOT-02 | defaultOutstanding operation should send bAsset to BPOOL after default | Held |
DFLOT-03 | Total credit issued should not change | Held |
DFLOT-04 | Total collateralized should not change | Held |
DFLOT-05 | Last defaulted timeslot should be today | Held |
DFLOT-06 | User’s reserve balance should not change | Held |
DFLOT-07 | User’s bAsset balance should not change | Held |
DFLOT-08 | Total interest accumulated should not change | Held |
DFLOT-09 | Floor position reserve should not change | Held |
DFLOT-10 | Anchor position reserve should not change | Held |
DFLOT-11 | Discovery position reserve should not change | Held |
DFLOT-12 | Credit facility reserve balance should not change | Held |
DFLOT-13 | Credit facility bAsset balance should not change | Held |
DFLOT-14 | BPOOL reserve balance should not change | Held |
DFLOT-15 | CREDT reserve balance should not change | Held |
DFLOT-16 | CREDT bAsset balance should not increase | Held |
DFLOT-17 | DefaultOutstanding should not revert | Held |
DFSF-01 | Account credit should be greater than zero for successful defaultSelf operation | Held |
DFSF-02 | DefaultSelf should set the caller's account credit to zero | Held |
DFSF-03 | DefaultSelf should set the caller's account collateral to zero | Held |
DFSF-04 | DefaultSelf should set the caller's account expiry to zero | Held |
DFSF-05 | DefaultSelf should reduce total credit issued by the account's credit | Held |
DFSF-06 | DefaultSelf should reduce total collateralized by the account's collateral | Held |
DFSF-07 | DefaultSelf should send defaulted bAsset to BPOOL after default | Held |
DFSF-08 | DefaultSelf shouldn't revert if the credit in the user’s account is greater than zero | Held |
MM-01 | Anchor liquidity should always be thinner than discovery | Held |
MM-02 | Checkpoint tick should never be below the floor | Held |
MM-03 | Active tick should never be below the floor | Held |
MM-04 | Solvency invariant: total capacity should never be less than circulating supply | Held |
MM-05 | Discovery Liquidity Is Always Greater Than 0 | Held |
MM-06 | Positions Ticks Always Multiple Of Tick Spacing | Held |
MM-07 | Verify that the liquidity in the PositionData for each range should correspond to the actual | Held |
CREDT-01 | liquidity in the Uniswap V3 pool for that range. The amount of bAsset in CREDT should match the total collateralized | Held |
CREDT-02 | The lastDefaultedTimeslot should not be ahead of the current day | Held |
CREDT-03 | The sum of all individual collateral amounts should equal the total total collateralized. | Held |
CREDT-04 | The sum of all individual credit amounts should equal the total credit issued. | Held |
CREDT-05 | A user's credit account should either have both credit and collateral or neither. | Held |
CREDT-06 | Baseline value of total collateral should be greater than credit provided | Held |
CREDT-07 | The value of a borrower's credit should never be higher than the value of their collateral | Held |
More from Baseline Markets
All 12 reports-
Mercury, Round 3
109 findings4 critical · 9 high 109 findings: 4 critical, 9 high, 28 medium, 33 low, 35 informational -
AMM, Round 2
47 findings4 critical · 14 high 47 findings: 4 critical, 14 high, 8 medium, 13 low, 8 informational -
AMM
54 findings3 critical · 6 high 54 findings: 3 critical, 6 high, 13 medium, 11 low, 21 informational -
Fixed Supply
34 findings4 high 34 findings: 4 high, 10 medium, 20 low
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
