Baseline engaged Guardian to review the security of its liquidation-free perpetual leverage system. From the 13th of August to the 20th of August, a team of 3 auditors reviewed the source code in scope.
- Published
- Review window
- August 13 to 20, 2024
- Language
- Solidity
- Chains
- Blast
- Sector
- Token launches
- 0 Critical
- 6 High
- 4 Medium
- 10 Low
- 0 Informational
Scope
-
github.com/guardianaudits
110fc8cfe1b8f022c0fd
Overview
Baseline engaged Guardian to review the security of its liquidation-free perpetual leverage system. From the 13th of August to the 20th of August, a team of 3 auditors reviewed the source code in scope.
Findings 20
-
H-01 High Dangerous setFundingRate Function Logical Error Resolved
Description
The
setFundingRatefunction allows a trusted address to assign the funding rate for the vault, however in almost any circumstance where thefundingRateis updated it will invalidate the funding accounting of the vault.For example:
fundingRateis 10% per year- Position A is opened at year 0 with X collateral
- Position B is opened at year 1 with X collateral
- The vault experiences decay for 1 year collateral: X * 1/e^0.1 = X * 1/1.105 and then gains X collateral
- Vault collateral is now X * (1 + 1/1.105) = 1.905X
- The funding rate is set to 5% per year
- Position A is closed at year 2, the position experiences decay for 2 years at a rate of 5% per year, collateral:
X * 1/e^0.1 = X * 1/1.105 = 0.905X
- The vault experiences decay for 1 year at 5% collateral: 1.905X * 1/e^0.05 = 1.905X * 1/1.051 = 1.813X
- Position A’s collateral is now removed from the vault, collateral left is 1.813X - 0.905X = 0.908
- Position B is closed at year 2, the position experiences decay for 1 year at a rate of 5%, collateral: X *
1/e^0.05 = X * 1/1.051 = 0.951X
- Position B’s collateral is attempted to be removed from the vault, but the vault only has 0.908X collateral
left so the position cannot be fully closed.
The core issue is that every position must be updated to agree with the decay experienced by the vault, if the rate changes then every position must be updated along with the vault to decay at that time with the previous rate.
Recommendation
Updating every position to update the rate is not feasible in an EVM environment, consider restructuring the decay to rely on a single
fundingDecayAccwhich accumulates for every position in the vault, similar to arewardsPerSharemodel.Every position can be stamped with a last
FundingDecayAccand the decay can be measured as the difference between thelatestFundingDecayAcc() - position.lastFundingDecayAcc.Then the
setFundingRatefunction can simply update the lastFundingDecayAccusing the previous rate before assigning the new rate.Resolution
Baseline Team: We remediated this by using shares and an ever increasing index to calculate interest.
-
H-02 High closePosition May Break Through The Floor Logical Error Resolved
Description
The
closePositionfunction swapsbAssetsfor reserves before adding reserves back to the floor position, therefore it is possible for thebAssetsell to break through the floor tick and invalidateBLV.Recommendation
At the end of
closePositionrevert if_tradingInFlooris true, similar to in_deleverage.Resolution
Baseline Team: Resolved.
-
H-03 High Crucial Storage Overwritten On configureDependencies Access Control Resolved
Description
In the
configureDependenciesfunction thesweepTick,slideTick, andlastDropTimestampare assigned to their initial values. However there is no access control that prevents theconfigureDependenciesfunction from being called again.Recommendation
Consider only assigning these values on the first call to
configureDependencies, and if necessary include a separate trusted function to re-assign the values.Resolution
Baseline Team: Resolved.
-
H-04 High Loops Vault Decay Invalidates Solvency Check DoS Resolved
Description
Proof of concept: PoC
The Loops vault decays the debt of every loop position over time and this decay is reported by the
totalDebt, however thetotalDebtfunction does not reduce the total circulating supply corresponding to the decay of the total position collaterals.As a result the decay will invalidate the solvency check and DoS all functionality until funding has been charged.
Recommendation
Charge funding before completing every action in the system that relies on the solvency check, or consider accounting for the circulating supply that would decrease from the decay in the solvency check.
Resolution
Baseline Team: We’ve added charge funding to every action in
MarketMakingandCreditFacility. -
H-05 High Trapped Fees In LoopFacility Logical Error Resolved
Description
In the loop facility fees are often collected to the contract with the
_pullReservesfunction, however there is no functionality to retrieve these fees.Recommendation
Implement a function similar to the
setFeeRecipientfunction in theCreditFacilityto retrieve these fees.Resolution
Baseline Team: Resolved.
-
H-06 High Missing Blast Configurations Best Practices Resolved
Description
In the
LOOPSv1module there is no configuration for blast yields in the constructor.Recommendation
Add blast yields configuration to the constructor.
Resolution
Baseline Team: Resolved.
-
M-01 Medium MarketMaking Ignores Loops Capacity Logical Error Resolved
Description
Additional debt can now serve as capacity for the
Baselinesystem in the Loops vault. This is accounted for in theCreditFacilitybut not in theMarketMakingcontract.As a result the capacity checks between the
CreditFacilityandMarketMakingpolicies will not agree.Recommendation
Include the
LOOPS.totalDebt()when computing the capacity in theMarketMakingcontract.Resolution
Baseline Team: Resolved.
-
M-02 Medium Vault Position Not Sum Of User Positions DoS Resolved
Description
Function
getFundingSinceis not perfectly precise, such that the decay of two time deltas X and Y is not the same as the funding decay of one time delta X + Y. This is important sincechargeFundingonly updates the last update timestamp for the vault position, not user positions.Consider this scenario where there is only one open position: 1) 10 seconds pass. 2) Vault is charged funding for 10 second decay. 3) 200 more seconds pass. 4) Vault is charged funding for 200 second decay; User is charged funding for 210 second delay. 5) User sends request to close their position.
Ultimately, the latest position of the vault is not aligned with the latest position of the user due to the imprecision of
getFundingSince. The position the user can reduce is greater than the latest vault position, causing an underflow when performingvault.position -= _positionToReduce.This can be harmful in the case there are multiple open positions, and a single depositor is left hanging and unable to close their position.
Note that this issue is also applicable to the
vault.debt -= debtToReduce_;calculation as the debt is also updated when funding is charged.Recommendation
Change the reduction to:
uint256 amtPosToReduce = vault.position < _positionToReduce - vault.position : _positionToReduce; vault.position -= amtPosToReduce; uint256 amtDebtToReduce = vault.debt < debtToReduce_ - vault.debt : debtToReduce_; vault.debt -= amtDebtToReduce; bAsset.transfer(msg.sender, amtPosToReduce)Resolution
Baseline Team: Since we are no longer decaying users debt separately from the vaults debt this should not be an issue.
-
M-03 Medium Slide Causes Anchor To Disappear Warning Acknowledged
Description
In the slide function the reserves of the anchor position are added back to the anchor after potentially extending the Anchor further downwards with a call to
_updateTicks.This can result in the Anchor position having little liquidity or disappearing entirely after the slide operation. This is because the price may have been set less than or equal to the lower end of the Anchor position, in which case there would be no reserves in the liquidity position.
In this case the Anchor position would not be built up again until a sweep occurs, and in the meantime there can be erratic price fluctuations between the floor and discovery which may be far apart.
Recommendation
In these situations consider keeping the Anchor position to the
tickSpacingabove the current price so that the Anchor does not entirely disappear, but is not extended below the active price because that would require reserves to be pulled from the floor.Otherwise be aware of this quirk in the system and document it for users and integrators.
Resolution
Baseline Team: Acknowledged.
-
M-04 Medium Incorrect Virtual Reserves Accounting Validation Resolved
Description
In the launch function the
pessimisticCapacityincludes the floor reserves when stretching the virtual reserves over the entire floor position to compute its worst case capacity.This incorrectly accounts for stretching out the floor reserves which would actually increase if the price were to rise to the upper floor tick.
This was the original reason why the virtual reserves had to be stretched because they would not receive corresponding reserves in as price rose to the upper tick of the floor.
This accounting is attempted to be fixed by leaving the
bAssetsof the floor position in the circulating supply, as if they had been swapped out of the floor as price rose.However this again does not account for the reserves of the floor increasing due to swap input amounts.
Recommendation
Remove the special accounting for the floor position and revert to the original solvency check that was previously present in the launch function, with the one addition of the
LOOPS.totalDebt()value in thepessimisticCapacityaccounting.Resolution
Baseline Team: Resolved.
-
L-01 Low Unnecessary tradingInFloor Case Optimization Resolved
Description
Since the
floor.bAssetswill only be nonzero if the price is inside of the floor, thefloor.bAssetscan just be removed from the subtraction ofBPOOL.totalSupplyinstead of using the special_tradingInFloorcase.Recommendation
Remove the special case handling and remove the
floor.bAssetsfrom the subtraction ofBPOOL.totalSupply.Resolution
Baseline Team: Resolved.
-
L-02 Low Lacking Use Of Tick Spacing Constant Best Practices Resolved
Description
In the
getCurrentThresholdthe full tick spacing below thesweepTickis computed, however the computation uses a direct subtraction of 200 rather than theT_Sconstant which is determined by theBPOOL.Recommendation
Use the
T_Srather than a hardcoded spacing of 200.Resolution
Baseline Team: Resolved.
-
L-03 Low Anchor Can Exceed Defined Width Documentation Acknowledged
Description
The slide function will now no longer move the
anchorUpper/discoveryLowertick down, and instead only extend the lower tick of the anchor range downwards.This is because the
_updateTicksfunction will not update thesweepTickbut will set the lower anchor tick as the anchor width below theactiveTSwhich has indeed changed.Recommendation
This may be expected behavior, if so then consider documenting clearly that the Anchor can exceed the defined width.
Resolution
Baseline Team: Acknowledged.
-
L-04 Low Anchor Ticks Crossed In Drop DoS Resolved
Description
In the drop function when the tick range is assigned to the Anchor position in
_decrementSweepTickit is possible for thetargetSweepTickto be lower than theanchorTickLin rare cases where there is a large gap between the Anchor and Floor ranges which price has traversed.This will result in an
InvalidTickRangerevert and disallow the drop from occurring. The workaround is to simply call slide before dropping so that the anchor can sufficiently extend downwards.Recommendation
Consider adding this case to the
return falsecase in_decrementSweepTickto explicitly revert with theCannotDropDiscoveryerror or consider adding a revert specific to cases whereslidemust be called first.Resolution
Baseline Team: Resolved.
-
L-05 Low Position Can Increase Without Debt Logical Error Resolved
Description
If
_totalCollateralis a very small value such as 20 wei,debt_ =_totalCollateral.mulWad(BPOOL.getBaselineValue());will output 0 due to precision loss, and the position will increase without a corresponding increase in debt.Recommendation
Consider validating that the debt is non-zero when opening a position.
Resolution
Baseline Team: Resolved.
-
L-06 Low Typo Typo Resolved
Description
The documentation for function
openPositioncontains the typoposisblewhich should be updated topossible.Recommendation
Update the typo.
Resolution
Baseline Team: Resolved.
-
L-07 Low Zero Transfer DoS DoS Resolved
Description
In the
openPositionfunction a refund is issued to the user when the maximum reserves are not fully used, however even when thereservesIn_ == _maxReservesInthe transfer is still initiated.For some reserve tokens this may cause a revert due to the zero amount transfer. Additionally another potential zero transfer occurs in the
closePositionfunction on line 190.Recommendation
Add an if case to both of these transfers so that they are only attempted if there is a nonzero transfer amount.
Resolution
Baseline Team: Resolved.
-
L-08 Low Unexpected Deployment Behavior Configuration Resolved
Description
When deploying the new
MarketMakingpolicy theslideTickwill be assigned to the current active tick spacing.However this tick spacing may be in the current Discovery range, which can lead to a minor unexpected state.
Recommendation
Be sure to deploy the new
MarketMakingsystem when the active price is within the Anchor position.Resolution
Baseline Team: Resolved.
-
L-09 Low Infinite Slide Glitch Warning Resolved
Description
When the
activeTickis on an even tick spacing and theslideTickis the direct tick spacing above then a user can invoke a slide over and over again.This is because the criteria for a slide is:
activeTick <= slideTick - TSAnd_updateTicksperforms:slideTick = activeTS.Recommendation
Consider requiring
activeTick < slideTick - TSto trigger a slide.Resolution
Baseline Team: Resolved.
-
L-10 Low Unsafe Casting Casting Resolved
Description
Inside
_decrementSweepTickthe following calculation is performed:uint256 discoveryPremiumTS =uint256(uint24((sweepTick - activeTS) / T_S));The issue is that the
uint24may potentially cast a negative value, causing silent overflow.For Example:
sweepTick= -64400activeTS= -64200- Difference = -200
(sweepTick - activeTS) / T_S)= -1uint24(sweepTick - activeTS) / T_S)= uint24(-1) = 16777215
The
discoveryPremiumTSbecomes much larger than it should be, which leads to an overflow when performing(tickSpacingsToDecay * T_S).Consequently, dropping liquidity is prevented from occurring due to panic overflow.
Recommendation
Consider using
SafeCast.Resolution
Baseline Team: Resolved.
No findings match.
Invariants 29
The review's fuzzing suite asserted 29 invariants. 27 held and 2 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
OP-01 | Total position must increase accordingly on open | Held |
OP-02 | User position must increase accordingly on open | Held |
OP-03 | Debt should never decrease on open | Held |
CP-01 | Total position must decrease accordingly on close | Held |
CP-02 | Total position should not underflow on close | Held |
CP-03 | User position must decrease accordingly on close | Held |
CP-04 | Debt should never increase on close | Held |
CP-05 | Close should never decrease user reserve balances | Held |
CP-FAIL | closePosition should not revert with Insolvent error if not in floor | Broken |
DROP-01 | Anchor tick matches sweep tick post-drop | Held |
DROP-02 | Anchor lower tick stays the same post-drop | Held |
DROP-03 | Floor range stays the same post-drop | Held |
DROP-04 | Discovery upper and lower ticks should not be greater than their prior ticks. | Held |
DROP-05 | Discovery range size should not change | Held |
DROP-06 | Anchor liquidity stays the same post-drop | Held |
DROP-07 | Floor reserves should not decrease post-drop within delta | Held |
DROP-08 | Floor capacity should not decrease post-drop within delta | Held |
DROP-09 | Floor liquidity should not decrease post-drop within delta | Held |
DROP-10 | Reserves in the anchor position should be the same (or within 1 wei) before and after | Held |
LF-01 | a drop call System should be solvent after charging funding | Broken |
EXTEND-01 | newDurationDays_ and accountBefore.expiry must be valid for | Held |
EXTEND-02 | successful extend New expiry return data after borrow is called should match creditors account | Held |
EXTEND-03 | details Expiry must be in the future | Held |
EXTEND-04 | Expiry should increase after extension | Held |
EXTEND-05 | Credit can't be higher than collateral - before | Held |
EXTEND-06 | Credit can't be higher than collateral - after | Held |
EXTEND-07 | BPOOL should hold no reserve in its balance within delta | Held |
MM-05 | Should never be able to sweep and slide at the same time | Held |
MM-06 | slideTick should always be less than or equal to the sweepTick | 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.
