Baseline engaged Guardian to review the security of its concentrated liquidity protocol, supporting a baseline value for its YES token. From the 27th of May to the 6th of June, a team of 5 auditors reviewed the source code in scope.
- Published
- Review window
- May 27 to June 6, 2024
- Language
- Solidity
- Chains
- Blast
- Sector
- Token launches
- 0 Critical
- 12 High
- 6 Medium
- 10 Low
- 0 Informational
Scope
Overview
Baseline engaged Guardian to review the security of its concentrated liquidity protocol, supporting a baseline value for its YES token. From the 27th of May to the 6th of June, a team of 5 auditors reviewed the source code in scope.
Findings 28
-
H-01 High DoS Via External Liquidity By Predicting Positions DoS Resolved
Description
Proof of concept: PoC
Users adding external liquidity to the protocol's positions should not affect the market making operations, as the liquidity for each range is tracked by the
getLiquiditymapping in theBPOOLcontract storage.The issue relies on the way this mapping is updated after an operation completes. When using
addReservesTooraddLiquidityTothis mapping will be updated with the new real position liquidity, including externally added liquidity.An attacker can DoS the
sweepoperation by following these steps:- Add ext liquidity to the projected ANCHOR range after a bump (lowerTick + 1 TS, upperTick).
- Execute
bump(), now the ANCHOR range is the same as the projected one, which we just
maliciously added liquidity to. 3. Now the
addLiquidityTofunction call for the ANCHOR range records the malicious liquidity as belonging to the system. 4.sweepnow reverts with underflow.Recommendation
Remove any liquidity that is sitting in positions that will be used, but aren’t currently, and add this amount to the
bufferedReservesvalue.Resolution
Baseline Team: The issue was resolved in PR#90.
-
H-02 High Slide Allows Anchor To Exceed Discovery Logical Error Resolved
Description
When removing and adding back liquidity to the anchor position in the slide function, the
anchorReservesare tracked and added back to the adjusted anchor position.However the anchor position has moved leftwards after the slide, therefore the liquidity for the anchor position will increase as the reserves are maintained.
As a result, since the new discovery liquidity cannot increase past the old discovery liquidity, the protocol can enter a state where the anchor liquidity is greater than the discovery liquidity.
Recommendation
Consider the following resolution options:
- No longer require the new discoveryL ≤ old discoveryL
- Switch back to maintaining the liquidity for the anchor, rather than the reserves
Resolution
Baseline Team: The issue was resolved in PR#files#diff-3579424c9b33337cc1a4bf4454c2c5a502df76dc22c5110b8767b5f976fee172R312-R3 14.
-
H-03 High getLiquidityForReserve DoS DoS Resolved
Description
In the
getLiquidityForReservesfunction there is no validation that the lower price and upper price provided to thegetLiquidityForAmount1function are not equal. This is possible either with an anchor range that has no width or when the active price is exactly that of the lower tick price for a range. This behavior results in a DoS of several key functionalities of the system when these edge cases are hit.Recommendation
Implement the following validation such that the upperPrice can never be less than or equal to the lower price:
function getLiquidityForReserves( uint160 _sqrtPriceL, uint160 _sqrtPriceU, uint256 *reserves ) public view returns (uint128 liquidity*) { (uint160 sqrtPriceA,,,,,,) = pool.slot0(); uint160 upperPrice = min(_sqrtPriceU, sqrtPriceA); if (upperPrice <= _sqrtPriceL) { return 0; } liquidity_ = LiquidityAmounts.getLiquidityForAmount1( _sqrtPriceL, upperPrice, _reserves ); }Resolution
Baseline Team: Resolved.
-
H-04 High Leverage And Deleverage Are Sandwhichable Sandwhich Attack Acknowledged
Description
In the leverage and deleverage functions traders flash borrow and repay using a swap where the
maxAmountInis the borrowed amount for borrows and the unlocked collateral amount for repayments.This allows malicious actors to frontrun these actions and push the price far enough such that these limits are hit. In the case of borrowing, this leaves the borrower with no funds received from the action as the
reservesNeededcan be push to the maximum of the entirenewPrincipal_.Recommendation
Allow the user to configure an
amountInMaximumthemselves, and do not allow thisamountInMaximumto be greater than the borrowed amount for borrows and the unlocked collateral amount for repayments.Additionally, consider allowing the user to configure the
sqrtPriceLimitX96and deadline on the swap for additional MEV protection.Resolution
Baseline Team: As the original deployment is planned for Blast this is not an immediate issue, but will be addressed before subsequent chains are supported.
-
H-05 High removeAllFrom DoS With External Liquidity DoS Resolved
Description
In the
removeAllFromfunction, the function is short circuited if there is 0liquidityToRemove, however this value is based upon the amount that is deployed in Uniswap.Therefore a malicious actor may add to this amount in order to stop this early return check from triggering when the
getLiquiditymapping reports that there is no liquidity deployed there for the system.In this scenario the function execution continues on to remove all of the attacker’s malicious liquidity from the range and then attempts to remove 0 liquidity from the range after it has been depleted. This second attempt to remove liquidity will revert with the
NPerror from Uniswap.Recommendation
Add a second early return case right before the
_removeLiquidityinvocation:if (liquidityToRemove == 0) return (0, bAssetFees_, 0, reserveFees_);Resolution
Baseline Team: Resolved.
-
H-06 High Migration Cannot Be Done DoS Resolved
Description
During the migration process, the owner will construct the initial distribution of spot and credits and it will be done with
allocatefunction. This can only be called before the pool is launched, and the pool will be deployed later.Users might have both spot and credit to be distributed. But a user can also have only spot or only credit. However, the check that ensures the allocation is not empty is incorrect, and the
allocatefunction will revert if a user has only spot or only credit.The whole migration process will be disrupted even if just one user has only spot or only credit.
Recommendation
Change this line
if (_spot[i] == 0 || _collateral[i] == 0) revert InvalidAllocation();to this:
if (_spot[i] == 0 && _collateral[i] == 0) revert InvalidAllocation();Resolution
Baseline Team: The issue was resolved in commit b083ad0.
-
H-07 High Premium Compounding Is Incorrect Logical Error Resolved
Description
Proof of concept: PoC
The protocol deploys liquidity to ranges based on a liquidity premium and this premium supposed to be compounding when the difference between the
activeTickand thefloorTickLincreases.However, due to incorrect usage of the
powWadin thegetCurrentThresholdfunction, premium decreases instead of increasing. The reason of this behaviour is the numerator (1e16) in the formula is smaller than1e18and it causes result to decrease in every power.As a result of this, all liquidity managements in
sweepandslidewill be incorrect, and the maximum liquidity in discovery range will never pass1.1 * anchor liquidityduring these operations.Recommendation
Ensure the formula compounds the premium in a positive way while using the
powWad, while being sure to implement the correct test coverage for the liquidity threshold, as this bug was missed by the existing testing coverage.Resolution
Baseline Team: Resolved.
-
H-08 High bAsset Shorts Can Make A Guaranteed Profit Gaming Acknowledged
Description
Proof of concept: PoC
In the Baseline V2 system it is possible for a user to arbitrage the system in order to make guaranteed profit on a short.
This arbitrage can occur with the following actions:
- Bob borrows X bAssets from Alice
- Bob buys Y bAssets to move the anchor up after sweeping
- Bob sells Y bAssets and achieves a lower pool price than before his buy because:
- The anchor has moved up, meaning there is less capacity for the same amount of liquidity (though
in many cases this is offset by discovery surplus) 2. On his buy he was buying in the discovery (higher liquidity) and on his sell he was selling in the anchor (lower liquidity) 4. Slide moves the discovery liquidity back down so that Bob is able to buy the bAssets back at a lower average price, due to higher liquidity at a lower price. 5. Bob can pay back the bAssets he borrowed from Alice while the bAsset price is lower than when he borrowed, representing a profitable short. Bob has made a guaranteed profit of the delta on this short.
Recommendation
Consider restructuring the way liquidity is managed in the Baseline system, such that there are no immediate large liquidity shifts which can be arbitraged in this way.
Resolution
Baseline Team: In a future iteration a gradual liquidity shift will be implemented.
-
H-09 High Deleverage Can Break Through Floor Tick Logical Error Resolved
Description
When deleveraging an account, the floor tick can be breached because the account’s borrowed reserves have not yet been added to the floor tick, therefore the capacity cannot handle the flash swap — which assumes there is enough liquidity to sell the bAsset collateral before the borrowed reserves have been re-added.
Recommendation
Consider adding a requirement to the deleverage function that the existing liquidity structure, not including the virtual floor liquidity, can comfortably handle the sell of the bAsset collateral. Otherwise remove the deleverage feature and require users to deleverage manually, or implement a periphery contract which performs naive deleverages, without flash swapping, on behalf of the user.
Resolution
Baseline Team: The issue was resolved in PR#files.
-
H-10 High Borrows In The Floor Invalidate Baseline Value DoS Resolved
Description
Upon borrowing reserves are transferred out of the floor position and are accounted for in the virtual reserves. When a borrow occurs within the floor position, the reserves removed from the floor will not contribute towards the floor liquidity, therefore not requiring additional reserves to enter the floor upon swaps which move the price upwards out of the floor.
As a result, when reserves are moved into the virtual reserves and price moves upwards out of the floor, the virtual floor reserves are increasingly stretched wider across the floor. As a result the virtual floor reserves offer a smaller amount of liquidity and capacity as the price increases.
This can lead to an invalidation of the capacity invariant of the system, which ultimately invalidates the baseline value of the bAsset.
Recommendation
Implement validation in the
_leveragefunction such that if the price were to rise to the upper tick of the floor and the capacity invariant would be invalidated, the borrow reverts. Be sure that this validation ignores any capacity gain that would come as a result of additional reserves in the Floor as price rises.Additional reserves that would enter the floor as price increases must be ignored as they can mask an invalidation of the capacity invariant that would occur at an intermediate tick before the upper tick of the Floor.
Resolution
Baseline Team: The recommendation was implemented in commit 102c7a.
-
H-11 High Shorts Profit Because Of Fixed Anchor Width Gaming Acknowledged
Description
Proof of concept: PoC
In the updated Baseline V2 system the anchor is now limited to a distinct maximum width. This introduces an arbitrage whereby shorts can make a guaranteed profit by moving price into the floor, over the liquidity gap between the anchor and floor, rebalancing liquidity to fill in the gap, and buying bAsset tokens back at a lower price as the liquidity for the discovery has filled in the previous gap in the liquidity structure.
Recommendation
Consider removing the maximum width to reduce the severity of this arbitrage. Otherwise be aware of this potential gaming and consider reducing the discovery liquidity further when it moves in to fill a previous gap — thereby reducing the profitability of this manipulation.
Resolution
Baseline Team: Acknowledged.
-
H-12 High Anchor Liquidity Can Be Removed Logical Error Acknowledged
Description
In the slide function the anchor position is allocated based on the reserves of the previous anchor. However the previous anchor may have zero reserves, since it is capped to a maximum width, and the price is able to go below the anchor, between the gap if another actor creates an outside position.
As a result, the slide function will read that the anchor has 0 reserves and assign the new anchor to have 0 reserves and thus 0 liquidity.
Recommendation
Consider adopting a liquidity based approach to assigning the anchor position in slide rather than a reserves based approach.
Resolution
Baseline Team: Acknowledged.
-
M-01 Medium Launch Allows For Anchor Larger Than Target Width Logical Error Resolved
Description
In the launch function the anchor position can be more than 10 tick spacings wide as the setTicks call for the anchor position assigns the lower tick as the floor tick + one tick spacing, and the upper tick as the even tick spacing ahead of the active tick.
Recommendation
Limit the anchor position to have a lower of max(_floorTickL + T_S, activeTS - ANCHOR_WIDTH*).*
Resolution
Baseline Team: The issue was resolved in PR#92.
-
M-02 Medium Floor Reserves Decrease After Sweep Logical Error Acknowledged
Description
In the
sweepfunction the liquidity for the anchor position is maintained, however the anchor position moves higher. Therefore more reserves will be required to maintain the same liquidity for the anchor position.Therefore if there is not enough profit from the discovery position to overshadow this discrepancy, reserves will be taken from the floor to sustain the anchor liquidity while moving the anchor up. This may be unexpected as it can unnecessarily reduce capacity by moving reserves up from the floor position to the anchor.
Recommendation
Consider implementing the sweep function such that it maintains the reserves of the anchor position rather than the liquidity of the anchor position with the implementation listed below. However if using this implementation be aware that this has a trade off, where the liquidity of the anchor position can now reduce upon sweeping, though by a trivial amount.
It may ultimately be fine to acknowledge this issue and keep the existing implementation as a scenario which causes more than a 10,000 wei liquidity difference has not been identified.
Resolution
Baseline Team: Acknowledged.
-
M-03 Medium Swap Fees Included In Circulating Supply Logical Error Acknowledged
Description
The protocol deploys liquidity to Uniswap and earns fees in both reserve token and
bAsset. The protocol ownedbAssetsand earnedbAssetfees are not intended to be part of the circulating supply.However, swap fees in
bAssetare calculated as a part of the circulating supply duringbump,sweepandslide. The reason of this is circulating assets is calculated withbAssetsCirculating =BPOOL.totalSupply() - BPOOL.balanceOf(address(BPOOL));.Previously,
bAssetfees were in the BPOOL and they were excluded while subtracting the balance of BPOOL. But after remediations, those fees are in theMarketMakingcontract and not excluded. The newly issued tokens duringbumpis calculated based on thebAssetsCirculating, and more tokens will be issued due to this.Recommendation
Consider subtracting the fees stored in the
marketMakingcontract while calculatingbAssetsCirculating.Resolution
Baseline Team: Acknowledged.
-
M-04 Medium Interest Free Credits Intra-Day Gaming Resolved
Description
The
getTimeslotfunction returns the unix timestamp at the end of the day astoday, and this is used while calculating daily interests during credit borrowing. For example, when the current time is14:05:00, today is considered as23:59:59. Because oftodayis considered as the end of the day, the time between 23:59:59 and the exact borrow time are interest free.If a user gets a credit on June 1st at
00:00:01for only 1 day:- Today is June 1st
23:59:59 - Credit end time is June 2nd
23:59:59
User will basically get 2 days credit by paying only 1 day interest.
Recommendation
Consider adding a day to the
newExpiry_when first initializing a loan to account for the remainder of the current day. Otherwise, clearly document this behavior to users.Resolution
Baseline Team: The issue was resolved in commit 59118f1.
- Today is June 1st
-
M-05 Medium Extra Interest Charged When Extending Credit Logical Error Acknowledged
Description
The remediation of M-H-03 is performed via adding current day to remaining days when a user extends their credit. However, the parent finding was only causing problem when a new credit is issued with
0added days in the last day.Current remediation charges additional one day credit when users extend their credit regardless of when the extension is performed.
Recommendation
Consider not allowing users to borrow more in the last day with
0added days to fix the parent finding, rather than adding 1 day during every credit extension.Otherwise, clearly document this behavior to users.
Resolution
Baseline Team: Acknowledged.
-
M-06 Medium Decrease Position Prevented Logical Error Acknowledged
Description
The anchor and floor positions can be disjoint, and price may be between the two positions. When a user attempts to
decreasePosition, the floor reserves will be removed before the swap.If price is between the two positions, Uniswap will revert during the swap since there is no liquidity below the anchor as the floor was removed temporarily. Consequently, the user will be unable to
decreasePositionand unwind their leverage.Recommendation
Sliding will correct this case, monitor such situations and slide as necessary.
Resolution
Baseline Team: Acknowledged.
-
L-01 Low getLiquidityForReserves Edge Case Not Handled Logical Error Resolved
Description
When the current price at
sqrtPriceAis below the_sqrtPriceL, thegetLiquidityForReservesfunction will misrepresent the result as spreading the specified_reservesamount across the range from [sqrtPriceA, _sqrtPriceL]when in fact the position at[_sqrtPriceL, _sqrtPriceU]has no reserves in it. While currently no usage of the getLiquidityForReserves function would be prone to this bug, any future use-case is at risk.Recommendation
Return 0 from
getLiquidityForReserveswhen thesqrtPriceAis less than the_sqrtPriceL.Resolution
Baseline Team: The issue was resolved in PR#93.
-
L-02 Low Incorrect Comment Documentation Resolved
Description
In the
setTicksfunction the comment on line 213 states that the function is going to “calculate the corresponding sqrt prices for the tick boundaries and save them”, however there is no computation of the sqrt prices in thesetTicksfunction.Recommendation
Update this comment to reflect what the
setTicksfunction does.Resolution
Baseline Team: Resolved.
-
L-03 Low Superfluous Capacity Calculation Optimization Resolved
Description
In the
getCapacityForLiquidityfunction the capacity is calculated using the Uniswap V3 peripherygetAmount0ForLiquidityfunction if thesqrtPriceA >= _sqrtPriceL, however if thesqrtPriceA ==_sqrtPriceLthe result is trivially zero.Recommendation
Do not waste gas to compute the
sqrtPriceA == _sqrtPriceLcase and alter the condition in thegetCapacityForLiquidityto be a strict greater than comparison ofsqrtPriceA > _sqrtPriceL.Resolution
Baseline Team: The issue was resolved in PR#files#diff-3579424c9b33337cc1a4bf4454c2c5a502df76dc22c5110b8767b5f976fee172R312-R3 14.
-
L-04 Low Unused LIQUIDITY_THICKNESS Variable Optimization Resolved
Description
In the MarketMaking file the
LIQ_THICKNESSimmutable variable is assigned in the constructor yet never utilized.Recommendation
Remove the unnecessary
LIQ_THICKNESSvariable.Resolution
Baseline Team: The issue was resolved in PR#files#diff-3579424c9b33337cc1a4bf4454c2c5a502df76dc22c5110b8767b5f976fee172R312-R3 14.
-
L-05 Low Potentially Dangerous liquidityToRemove Best Practices Resolved
Description
In the
removeAllFromfunction theliquidityToRemoveamount is always assigned to thecurrentLiquiditywhich comes from thegetLiquiditymapping entry. In the event that the liquidity in the V3 pool is less than the entry in the mapping this will cause an underflow revert and DoS many features of the protocol.While there is currently no identified scenario where the
getLiquiditymapping will disagree with the liquidity of the V3 position, it is safer to only assign theliquidityToRemoveto the current liquidity in the case whereliquidityToRemove > currentLiquidityand that liquidity amount is guaranteed to be deployed.Recommendation
Consider moving the
liquidityToRemove = currentLiquidityassignment inside of theif(liquidityToRemove > currentLiquidity)case.Resolution
Baseline Team: The issue was resolved in PR#files#diff-3579424c9b33337cc1a4bf4454c2c5a502df76dc22c5110b8767b5f976fee172R312-R3 14.
-
L-06 Low Lack Of safeTransfer Best Practices Resolved
Description
In the
_leveragefunction, when leveraging the reserve tokens are sent via transfer without checking the return values. However the reserve token may potentially be a token which chooses to silently return false upon failure to transfer.Recommendation
Use
safeTransferto transfer reserve tokens in the_leveragefunction.Resolution
Baseline Team: The issue was resolved in commit 062330e.
-
L-07 Low Outdated Comment Documentation Resolved
Description
In the slide function the comment on line 328 mentions that the function “caps the new discovery liquidity to the old discovery liquidity”, however this is no longer the case.
Recommendation
Remove the outdated comment.
Resolution
Baseline Team: The issue was resolved in PR#files#diff-3579424c9b33337cc1a4bf4454c2c5a502df76dc22c5110b8767b5f976fee172R312-R3 14.
-
L-08 Low Unused State Variables Superfluous Code Resolved
Description
MAX_LIQ_PREMIUMandLIQ_THICKNESSare defined as state variables and assigned in the constructor.These values should’ve been used while calculating anchor and discovery liquidity. However, they are never used in any of the contracts.
Recommendation
Ensure that these variables are used when necessary or consider removing them.
Resolution
Baseline Team: The issue was resolved in commit 662962a.
-
L-09 Low Inaccurate Return Value Name Best Practices Resolved
Description
Function
_swapExactOut()returnsuint256 amountOut, althoughrouter.exactOutputSingleactually returns theamountInnecessary to achieve the exact output.Recommendation
Change
uint256 amountOuttouint256 amountInResolution
Baseline Team: The issue was resolved in commit e9d33fc.
-
L-10 Low Repay Credit Delta Does Not Match Reserves Delta Documentation Resolved
Description
During a repay it is possible for external liquidity to be included in the
getLiquiditymapping entry. This is because on the repayBPOOL.addReservesTo(Range.FLOOR, _repayment);is called which will store the existing liquidity at the tick range.Consequently, the invariant that the change in credit after the repay matches the change in the floor reserves does not hold.
Recommendation
Consider removing this vector by removing floor liquidity and adding it back during repayment.
Resolution
Baseline Team: The issue was resolved in commit e9d33fc.
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 -
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.
