Baseline engaged Guardian to review the security of their Fixed supply updates. From the 17th of February to the 21st of February, a team of 6 auditors reviewed the source code in scope.
- Published
- Review window
- February 17 to 21, 2025
- Language
- Solidity
- Chains
- Base
- Sector
- Token launches
- 0 Critical
- 4 High
- 10 Medium
- 20 Low
- 0 Informational
Scope
Overview
Baseline engaged Guardian to review the security of their Fixed supply updates. From the 17th of February to the 21st of February, a team of 6 auditors reviewed the source code in scope.
Issues Detected Throughout the engagement 4 High severity issues were uncovered and promptly addressed by the Baseline team.
Findings 34
-
H-01 High Invalid Remaining Reserves Calculation Logical Error Resolved
Description
The
MarketMakingcontract verifies if the protocol can bump by simulating an increase in theblvTick, and later validating some conditions, likebumpedCapacity > circulating.The
bumpedAnchorCapacityis calculated based on the_getAnchorReserves. However, thebumpedFloorCapacityis mistakenly uses theremainingReservesas follows:int256 remainingReserves = _getVirtualReserves() + reserve.balanceOf(address(BPOOL));However, the balance of the
BPOOLcontains all reserves, as they were all removed from the ranges, soremainingReservesis actually equal tototalReserves.Recommendation
Subtract the reserves used to calculate the ANCHOR range to correctly determine how many reserves remain.
Resolution
Baseline Team: The issue was fixed in line MarketMaking.sol#L354.
-
H-02 High DoS Of Deployment DoS Acknowledged
Description
Proof of concept: PoC
Deploying with
vm.startBroadcast()will lead to each call happening as a separate transaction. This gives a user who was distributedbTokensthe opportunity to interact with the Uniswap pool prior to the first rebalance occurring.A malicious user can deploy liquidity below the desired
BLV. Then, they can swap to their new deployed liquidity range.This will mean the active tick is below the
BLVtick. Whenrebalance()is invoked, it will attempt to set the lower tick of the anchor range to theBLVtick and the upper tick will be calculated using the current active tick.Since the current active tick is below the
BLVtick,setTicks()will revert due toInvalidTickRange. This will DoS the deployment after the tokens are distributed and the contract has been deployed.Recommendation
Deploy inside a smart contract function, so that the deployment happens atomically.
Resolution
Baseline Team: Acknowledged.
-
H-03 High Extend Interest Stolen Logical Error Resolved
Description
The Interest accrued from the
extendfunction sits in theCreditFacilitycontract until the fee recipient removes it with their approval.However this poses an issue because the
_swapExactOutfunction transfers the entire contract balance of theCreditFacilityto theBPOOLcontract.As a result these fee amounts which are sitting in the
CreditFacilitycontract will be deployed into the protocol liquidity instead of collectable.Recommendation
In the
extendfunction, instead of transferring the reserve amount from the user to theCreditFacilitycontract, transfer the reserve amount to the fee recipient directly. Furthermore, be sure there are no other instances where reserves are left in theCreditFacilitycontract.Resolution
Baseline Team: The issue was fixed in line CreditFacility.sol#L437.
-
H-04 High All Credit Interests Are Deployed As Liquidity Logical Error Resolved
Description
The
CreditFacility _sendReservesfunction used to keep the interest reserves amount in theCreditFacilitycontract so that this amount could be removed by the fee receiver which is approved for theCreditFacility.However now, because the
removeAllFromfunction leaves all removed tokens in the BPOOL contract, the interest amount is not collected by the protocol and will instead be deployed back into the liquidity structure.Recommendation
Use
BPOOL.transferToken(reserve, feeRecipient, _interest);in the_sendReservesfunction. Additionally, remove thefeeRecipientapproval logic as it is no longer necessary.Resolution
Baseline Team: The issue was fixed in line CreditFacility.sol#L548.
-
M-01 Medium Missing Equality Operator Logical Error Acknowledged
Description
The
_tradingInFloorfunctions in Policies verify if the active tick is at or below the floor's upper tick. However, inBrouter, this function only contains<, so the check will succeed whenactiveTick ==tickU.Recommendation
Update the operator to
<=so the call reverts when active tick is exactly at theBLV.Resolution
Baseline Team: Acknowledged.
-
M-02 Medium Outdated Anchor Tick Used In canBump Logical Error Resolved
Description
The
canBumpfunction uses an outdatedanchorTickwhich has not been updated to reflect the current price which the protocol is rebalancing for.As a result the capacity calculations for the Anchor range are not accurate to what the capacity will actually be after the rebalance.
This will often result in bumping when bumps should not occur, which will often prevent a rebalance from occurring since the final capacity invariant cannot be held. Or, more rarely, not allowing bumps to occur when they ought to be.
Recommendation
Consider updating the
anchorTickto the latest that will be used in the rebalance.Resolution
Baseline Team: Resolved.
-
M-03 Medium Unable To Rebalance Above DISCOVERY_LENGTH Logical Error Resolved
Description
The
MarketMakingpolicy is mainly in charged of rebalancing the liquidity positions, whencanRebalanceis true.Normally, a rebalance will be triggered when price moved outside of the rebalance ticks and its within a certain range:
bool isWithinRange = activeTick > blvTick & activeTick < anchorTick + DISCOVERY_LENGTH;However, the
DISCOVERYliquidity will range from theanchorTickto theMAX_TICK. This suggests that rebalance is not possible when price is inside theDISCOVERYbut aboveanchorTick +DISCOVERY_LENGTH.Recommendation
Update the
withinRangeto include all theDISCOVERYrange so rebalance is possible when price is at that range:bool isWithinRange = activeTick > blvTick & activeTick < MAX_TICK;Resolution
Baseline Team: The issue was fixed in line MarketMaking.sol#L201.
-
M-04 Medium Temporary DoS Of openPosition() DoS Resolved
Description
The
isEthvalidation checks the balance of the contract instead of themsg.value. A malicious user can send ether to theBrouter, in order to trigger a refund toLoopFacilitywhenopenPosition()is called.Since
LoopFacilitydoes not have areceive()function, this will cause a call toopenPosition()to revert. The malicious user can then pull their ether out through a swap in the following transaction.Recommendation
Change the validation for
isEthfrom checking the contract balance to checking themsg.value.Resolution
Baseline Team: The issue was fixed in line Brouter.sol#L242.
-
M-05 Medium Capacity Errantly Increased Logical Error Acknowledged
Description
The
CreditFacilityallows users to borrow reserves withbAssetsas collateral. These reserves will be retrieved from the Liquidity Ranges.If there are not enough reserves in the
FLOOR, the excess amount will be taken from theANCHOR. However, when users repay reserves, these will be added to theFLOORonly.Due to the fact that there is no interest charged, users can deliberately borrow enough reserves to remove some from the
ANCHORrange and immediately repay.This will increase the capacity of the system as the reserves that were in the
ANCHORhad lower capacity than if they are valued at theblv. Additionally, this may open arbitrage opportunities, as well as DoS user actions, as liquidity in the trading range is moved down.Recommendation
Set interest greater than zero, to disincentivize whales from manipulating the reserves and capacity. Additionally, consider triggering a rebalance, if possible, to re distribute the reserves where they belong.
Resolution
Baseline Team: Acknowledged.
-
M-06 Medium External Liquidity Causes Trade Reverts Logical Error Resolved
Description
When trading in the floor, liquidity is removed from the floor during a buy in order to improve price movement.
However, an external user can provide liquidity in the floor range, making the trade still revert. This revert will prevent swaps that are bringing the price closer to the BLV from executing.
Recommendation
Instead, validate that the price has moved close to the BLV and check that the price is not in the floor for
closePosition().Resolution
Baseline Team: The issue was fixed in line Brouter.sol#L272.
-
M-07 Medium Rebalance Prevented Near Floor Tick DoS Resolved
Description
In the
_getACUfunction when the active pool price is just above theblvTickthere will be an overflow when attempting to cast the result ofFullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtRatioBX96 -sqrtRatioAX96)to a uint128 variable inside of thegetAmount0ForLiquidityfunction.As a result rebalances are DoS'd when the pool price is in this edge case range.
Recommendation
Be aware of this DoS and consider refactoring the leverage calculations to avoid calculating the ACU when the price is close to the
blvTickand instead returning a default asymptotic value.Resolution
Baseline Team: Resolved.
-
M-08 Medium Swap With Rebalances Errantly Used Logical Error Resolved
Description
In the
closePositionfunction theexactInputSinglefunction on theBRoutercontract is used which rebalances before and after the swap.This does not match the previous behavior of the
closePositionfunction and can prevent users from repaying their debts because the rebalance function may revert with anBackingInsolventerror.Recommendation
Use the
exactInputSingleVanillafunction instead.Resolution
Baseline Team: The issue was fixed in line LoopFacility.sol#L175.
-
M-09 Medium Donated Liquidity Not Given To Fee Recipient Logical Error Resolved
Description
In the
removeAllFromthe donated liquidity that was not deployed by the protocol is intended to be transferred to the fee recipient.However in the case where the ranges are updated and third party liquidity is found in the new ranges, the following early return is used in the
removeAllFromfunction:liquidityToRemove = currentLiquidity; if (liquidityToRemove = 0) return (0, bAssetFees_, 0, reserveFees_);In this case the transfers at the end of the
removeAllFromfunction are not made.reserve.safeTransfer(feeRecipient, reserveFees_); bAsset.transfer(feeRecipient, bAssetFees_);Recommendation
Inside the early return case, be sure to make the same transfers to the
feeRecipient.Resolution
Baseline Team: The issue was fixed in line BPOOL.v1.sol#L218.
-
M-10 Medium Failed Liquidity Deployment Due To Insufficient Balance DoS Resolved
Description
During the liquidity deployment phase of rebalancing,
DISCOVERYliquidity is added first, followed byANCHORliquidity. The threshold liquidity added toDISCOVERYis the minimum of multiple calculations:uint256(threshold_).min(uint256(bTokenLiquidityMax)).When the final threshold liquidity is
bTokenLiquidityMax, the entirebAssetbalance of theBPOOLwill be deployed to theDISCOVERYrange duringdeployLiquidityTo(DISCOVERY), asbTokenLiquidityMaxis calculated usingbalanceOf(BPOOL).However, adding liquidity to
ANCHORin the next step also requires somebAssetswhenactiveTick <anchorTick. Since the entirebAssetbalance has already been deployed to theDISCOVERYrange,addReservesTo(ANCHOR)fails duringuniswapV3MintCallbackdue to insufficient balance.Recommendation
Consider leaving a buffer amount when calculating
bTokenLiquidityMaxinstead of using the entire balance. This ensures that the contract retainsbAssetsto add toANCHOR, even whenbTokenLiquidityMaxis deployed toDISCOVERY.Resolution
Baseline Team: The issue was fixed in line MarketMaking.sol#L472.
-
L-01 Low Unused Code Optimization Resolved
Description
The following code is not used in the current implementation:
LoopFacility._tradingInFloor()BlastClaimer,IUniswapV3Pool,FixedPoint96import in LOOPS- debug code (
console2.sol,PoolViewerLib.sol)
Recommendation
Remove the unused code or add an implementation for it.
Resolution
Baseline Team: Resolved.
-
L-02 Low Lack Of Reentrancy Validation Reentrancy Acknowledged
Description
_swap()sends ether to the user viacall(), which will hand over the execution flow to thereceive()function if it is a smart contract. It is best practice to use reentrancy modifiers when this occurs.Recommendation
Add reentrancy guard modifiers to the swap functions.
Resolution
Baseline Team: Acknowledged.
-
L-03 Low Swaps Allowed To Non-Baseline Pools Validation Resolved
Description
_swap()does not validate the fee tier that is passed for a swap. This allows users to perform swaps with pools that have been created with the same tokens but set to different fee tiers.Recommendation
Validate the fee of the trade in the
_swap()function.Resolution
Baseline Team: The issue was fixed in line Brouter.sol#L234.
-
L-04 Low Superfluous balanceOf Call Optimization Resolved
Description
The
_removeLiquiditycontains a call tobAsset.balanceOf(address(BPOOL));whose return value is not used.Recommendation
Remove the
balanceOfcall.Resolution
Baseline Team: Resolved.
-
L-05 Low Payer Param No Longer Used Optimization Resolved
Description
The
BPOOLcontract will always own the reserves andbAssetsused to add liquidity to ranges, so in theuniswapV3MintCallbackthese tokens are transferred directly to the pool.Therefore, there is no need to encode the
payerormsg.senderduringpool.mintas this encoded data is not longer used.Recommendation
Send empty data in the last param in
pool.mintResolution
Baseline Team: Resolved.
-
L-06 Low ANCHOR Range Disappears Validation Resolved
Description
The rebalance action can now be executed when more than 8 hours have passed since the last rebalance, bypassing the other price and range checks.
This allows the ANCHOR range to disappear when the following scenarios are met:
activeTick=blvTick- (
activeTick<blvTick+ 199) & (liquidityA > _getThresholdLiquidity())
Although the first scenario might be expected, the second one might not, as it will create a DISCOVERY position with reserves.
Recommendation
Consider if this is the expected behavior and prevent rebalances to occur.
Resolution
Baseline Team: Resolved.
-
L-07 Low onlyKernel Modifier Discrepancy Validation Resolved
Description
The
onlyKernelmodifier prevents external calls to certain functions when Modules are installed or Policies are activated.However, this modifier is only used in certain cases, leaving some unprotected functions, like the
configureDependenciesin Policies, that can lead to unexpected scenarios.Recommendation
Consider adding the
onlyKernelmodifier to the all Module and Policies functions that should only be called by the Kernel contract.Resolution
Baseline Team: Resolved.
-
L-08 Low Misleading Burn Function Documentation Acknowledged
Description
The
CREDTmodule still contains a_burnDefaultedCollateral, but the bAssets are not burned anymore. Instead, they are transferred to theBPOOLmodule to be used for the next liquidity rebalance. Although the function does not actually burn, it can be misleading, as well as its natspec.Recommendation
Update the
_burnDefaultedCollateralfunction name as well as the comments, and avoid suggesting aburnResolution
Baseline Team: Acknowledged.
-
L-09 Low _canBump Early Return Optimization Resolved
Description
The rebalance operation, after removing liquidity, will try to check if the current liquidity structure accepts a
bump, which relies on certain conditions being met at the same time.One of these conditions is
tickDelta > BUMPABLE_PREMIUMwhich prevents bumps if the tick premium (difference between theactiveTickand theblvTick) is greater than 1500 (default value).Therefore, to save gas and avoid more calculations, the function should early return with
falseif this condition is not met.Recommendation
Early return
falseiftickDelta = BUMPABLE_PREMIUMResolution
Baseline Team: The issue was fixed in line MarketMaking.sol#L335.
-
L-10 Low Duplicated BLV Price Getters Optimization Acknowledged
Description
Both
BPOOL.getBaselineValue,LoopFacility.getBaselineValue()andMarketMaking.getBLV()calculate the current baseline value price based on the upper tick of the floor.Although they all return the same value, this can lead to issues in the future if one is updated but not the others.
Recommendation
Consider having one single source of truth for the
blvcalculation.Resolution
Baseline Team: Acknowledged.
-
L-11 Low Deadline Set To Block.timestamp Logical Error Acknowledged
Description
The
Brouterperforms swaps in theUniswapV3Pool, used by some Policies. The issue arises when usingblock.timestampas the deadline parameter for these swaps.A malicious block builder will be able to execute this at any time, when such transaction is useful for manipulating the price.
Recommendation
Add an optional deadline parameter to functions that routes swaps through the
Brouterand use this instead ofblock.timestampfor all the swaps.Resolution
Baseline Team: Acknowledged.
-
L-12 Low Misleading Documentation In getBaselineValue Documentation Resolved
Description
The
getBaselineValuecalculates theBTokenprice at upper tick of theFLOORrange. However, the documentation mentionsReturns the price at the lower tick of the floor position, which is misleading.Recommendation
Update the documentation to:
Returns the price at the upper tick of the floor positionResolution
Baseline Team: The issue was fixed in line BPOOL.v1.sol#L271.
-
L-13 Low Unusable DISCOVERY_LENGTH Superfluous Code Resolved
Description
The
DISCOVERY_LENGTHvalue is assignable by the owner address and is used incanBumpbut actually has nothing to do with the length of the discovery range as it is hardcoded to the max tick.Recommendation
Consider either removing the
DISCOVERY_LENGTHvariable or making the Discovery range configurable by it.Resolution
Baseline Team: Resolved.
-
L-14 Low _canBump Validation Does Not Round Correctly Rounding Resolved
Description
In the
_canBumpfunction there is validation to check if the circulating supply can be absorbed by the total reserves immediately after increasing theblvTick.if (totalReserves < circulating.mulWad(getBLV())) {blvTick = T_S; return false;}This validation multiplies the circulating supply by the BLV price using mulWad, which rounds down. This does not round in the protocol's favor as it is rounding down the circulating value that the reserves must cover.
This is in contrast to the same validation which is performed differently in the
_removeLiquidityfunction.uint256 maxCapacity = totalReserves.divWad(getBLV()); if (maxCapacity < circulating) {revert BackingInsolvent();}In the
_removeLiquidityfunction themaxCapacityis exposed to rounding down because it is thetotalReservesdivided by the BLV price withdivWadwhich rounds down.This is the correct way to perform this validation which rounds in favor of being more conservative about the capacity invariant.
Recommendation
Use the same validation as is performed in the
_removeLiquiditywhich rounds conservatively.Resolution
Baseline Team: Resolved.
-
L-15 Low Max Tick Discovery Liquidity Warning Warning Acknowledged
Description
Since the Discovery liquidity is now deployed to the max tick and the circulating supply is now limited, the liquidity achievable in the Discovery range will be somewhat limited.
In many cases this is will result in the capping of the threshold liquidity, which may give resistance to deploying the liquidity structure that is desired.
Recommendation
Be aware of this constraint and be prepared to adjust the discovery range as needed if outcomes are not as desired.
Resolution
Baseline Team: Acknowledged.
-
L-16 Low Missing payable On exactInputSingleVanilla Modifiers Resolved
Description
The
exactInputSingleVanillafunction does not have thepayablekeyword, unlike the other three functions, and therefore cannot perform swaps with the native token.Recommendation
Add
payableto this function as well.Resolution
Baseline Team: The issue was fixed in line Brouter.sol#L126.
-
L-17 Low Unnecessary Allowance Optimization Resolved
Description
The
MarketMakingcontract grants approval toBPOOLto spend reserve tokens. However, this approval is unnecessary, asBPOOLno longer invokestransferFromafter the updates.Recommendation
Remove floating allowances.
Resolution
Baseline Team: The issue was fixed in line MarketMaking.sol#L115.
-
L-18 Low Leverage Can Be Below 1x Warning Resolved
Description
In the
_getLeveragefunction the leverage is multiplied by 0.9999 to avoid any rounding up edge cases. However this allows the leverage to be lower than 1x, which may be unexpected.Recommendation
Consider enforcing a minimum value of 1e18 for the leverage result.
Resolution
Baseline Team: The issue was fixed in line MarketMaking.sol#L509.
-
L-19 Low Superfluous Comment Superfluous Code Resolved
Description
The comment related to spot supply invariant check at lines 410–411 of the
MarketMakingcontract still persists, even though the lineuint256 spotSupply = getCirculatingSupply() -LOOPS.totalCollateral() - CREDT.totalCollateralized()has been removed.Recommendation
Remove the unnecessary comment.
Resolution
Baseline Team: The issue was fixed in line MarketMaking.sol#L407.
-
L-20 Low Inability To Update BToken Controller Warning Acknowledged
Description
The
BTokencontroller is initialized with theBPOOLaddress during deployment, which is also set as the controller address.The
BToken.setControllerhas an access control validation so only the current controller address (BPOOL) can update it.However, the only function in
BPOOLthat can update the controller is themigrateBToken, which ispermissionedand there is no Policy that has the function permission.Recommendation
Consider adding
migrateBTokenpermission to the policy that should be allowed to call this function.Resolution
Baseline Team: Acknowledged.
No findings match.
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 -
bToken
8 findings 8 findings: 4 medium, 4 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.
