Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · September 2024

Loop Updates

for Baseline Markets

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

18 resolved · 2 acknowledged

Scope

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

  1. H-01 High Dangerous setFundingRate Function Logical Error Resolved
    Location
    Loops.v1.sol: 190

    Description

    The setFundingRate function allows a trusted address to assign the funding rate for the vault, however in almost any circumstance where the fundingRate is updated it will invalidate the funding accounting of the vault.

    For example:

    • fundingRate is 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 fundingDecayAcc which accumulates for every position in the vault, similar to a rewardsPerShare model.

    Every position can be stamped with a last FundingDecayAcc and the decay can be measured as the difference between the latestFundingDecayAcc() - position.lastFundingDecayAcc.

    Then the setFundingRate function can simply update the last FundingDecayAcc using 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.

  2. H-02 High closePosition May Break Through The Floor Logical Error Resolved
    Location
    LoopFacility.sol: 159

    Description

    The closePosition function swaps bAssets for reserves before adding reserves back to the floor position, therefore it is possible for the bAsset sell to break through the floor tick and invalidate BLV.

    Recommendation

    At the end of closePosition revert if _tradingInFloor is true, similar to in _deleverage.

    Resolution

    Baseline Team: Resolved.

  3. H-03 High Crucial Storage Overwritten On configureDependencies Access Control Resolved
    Location
    MarketMaking.sol: 154-157

    Description

    In the configureDependencies function the sweepTick, slideTick, and lastDropTimestamp are assigned to their initial values. However there is no access control that prevents the configureDependencies function 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.

  4. H-04 High Loops Vault Decay Invalidates Solvency Check DoS Resolved
    Location
    Global

    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 the totalDebt function 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 MarketMaking and CreditFacility.

  5. H-05 High Trapped Fees In LoopFacility Logical Error Resolved
    Location
    LoopFacility.sol

    Description

    In the loop facility fees are often collected to the contract with the _pullReserves function, however there is no functionality to retrieve these fees.

    Recommendation

    Implement a function similar to the setFeeRecipient function in the CreditFacility to retrieve these fees.

    Resolution

    Baseline Team: Resolved.

  6. H-06 High Missing Blast Configurations Best Practices Resolved
    Location
    LOOPS.v1.sol: 50

    Description

    In the LOOPSv1 module there is no configuration for blast yields in the constructor.

    Recommendation

    Add blast yields configuration to the constructor.

    Resolution

    Baseline Team: Resolved.

  7. M-01 Medium MarketMaking Ignores Loops Capacity Logical Error Resolved
    Location
    MarketMaking.sol: 553

    Description

    Additional debt can now serve as capacity for the Baseline system in the Loops vault. This is accounted for in the CreditFacility but not in the MarketMaking contract.

    As a result the capacity checks between the CreditFacility and MarketMaking policies will not agree.

    Recommendation

    Include the LOOPS.totalDebt() when computing the capacity in the MarketMaking contract.

    Resolution

    Baseline Team: Resolved.

  8. M-02 Medium Vault Position Not Sum Of User Positions DoS Resolved
    Location
    LOOPS.v1.sol

    Description

    Function getFundingSince is 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 since chargeFunding only 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 performing vault.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.

  9. M-03 Medium Slide Causes Anchor To Disappear Warning Acknowledged
    Location
    MarketMaking.sol: 410

    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 tickSpacing above 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.

  10. M-04 Medium Incorrect Virtual Reserves Accounting Validation Resolved
    Location
    BaselineInit.sol: 214

    Description

    In the launch function the pessimisticCapacity includes 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 bAssets of 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 the pessimisticCapacity accounting.

    Resolution

    Baseline Team: Resolved.

  11. L-01 Low Unnecessary tradingInFloor Case Optimization Resolved
    Location
    LoopFacility.sol: 252, 258

    Description

    Since the floor.bAssets will only be nonzero if the price is inside of the floor, the floor.bAssets can just be removed from the subtraction of BPOOL.totalSupply instead of using the special _tradingInFloor case.

    Recommendation

    Remove the special case handling and remove the floor.bAssets from the subtraction of BPOOL.totalSupply.

    Resolution

    Baseline Team: Resolved.

  12. L-02 Low Lacking Use Of Tick Spacing Constant Best Practices Resolved
    Location
    MarketMaking.sol: 545

    Description

    In the getCurrentThreshold the full tick spacing below the sweepTick is computed, however the computation uses a direct subtraction of 200 rather than the T_S constant which is determined by the BPOOL.

    Recommendation

    Use the T_S rather than a hardcoded spacing of 200.

    Resolution

    Baseline Team: Resolved.

  13. L-03 Low Anchor Can Exceed Defined Width Documentation Acknowledged
    Location
    MarketMaking.sol

    Description

    The slide function will now no longer move the anchorUpper/discoveryLower tick down, and instead only extend the lower tick of the anchor range downwards.

    This is because the _updateTicks function will not update the sweepTick but will set the lower anchor tick as the anchor width below the activeTS which 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.

  14. L-04 Low Anchor Ticks Crossed In Drop DoS Resolved
    Location
    MarketMaking.sol: 481

    Description

    In the drop function when the tick range is assigned to the Anchor position in _decrementSweepTick it is possible for the targetSweepTick to be lower than the anchorTickL in rare cases where there is a large gap between the Anchor and Floor ranges which price has traversed.

    This will result in an InvalidTickRange revert 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 false case in _decrementSweepTick to explicitly revert with the CannotDropDiscovery error or consider adding a revert specific to cases where slide must be called first.

    Resolution

    Baseline Team: Resolved.

  15. L-05 Low Position Can Increase Without Debt Logical Error Resolved
    Location
    LoopFacility.sol: 124

    Description

    If _totalCollateral is 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.

  16. L-06 Low Typo Typo Resolved
    Location
    LoopFacility.sol: 118

    Description

    The documentation for function openPosition contains the typo posisble which should be updated to possible.

    Recommendation

    Update the typo.

    Resolution

    Baseline Team: Resolved.

  17. L-07 Low Zero Transfer DoS DoS Resolved
    Location
    LoopFacility.sol: 151, 190

    Description

    In the openPosition function a refund is issued to the user when the maximum reserves are not fully used, however even when the reservesIn_ == _maxReservesIn the 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 closePosition function 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.

  18. L-08 Low Unexpected Deployment Behavior Configuration Resolved
    Location
    Global

    Description

    When deploying the new MarketMaking policy the slideTick will 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 MarketMaking system when the active price is within the Anchor position.

    Resolution

    Baseline Team: Resolved.

  19. L-09 Low Infinite Slide Glitch Warning Resolved
    Location
    MarketMaking.sol

    Description

    When the activeTick is on an even tick spacing and the slideTick is 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 - TS And _updateTicks performs:

    slideTick = activeTS.

    Recommendation

    Consider requiring activeTick < slideTick - TS to trigger a slide.

    Resolution

    Baseline Team: Resolved.

  20. L-10 Low Unsafe Casting Casting Resolved
    Location
    MarketMaking.sol: 465

    Description

    Inside _decrementSweepTick the following calculation is performed: uint256 discoveryPremiumTS = uint256(uint24((sweepTick - activeTS) / T_S));

    The issue is that the uint24 may potentially cast a negative value, causing silent overflow.

    For Example:

    • sweepTick = -64400
    • activeTS = -64200
    • Difference = -200
    • (sweepTick - activeTS) / T_S) = -1
    • uint24(sweepTick - activeTS) / T_S) = uint24(-1) = 16777215

    The discoveryPremiumTS becomes 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.

Invariants 29

The review's fuzzing suite asserted 29 invariants. 27 held and 2 did not.

Every invariant tested
IDInvariantResult
OP-01Total position must increase accordingly on openHeld
OP-02User position must increase accordingly on openHeld
OP-03Debt should never decrease on openHeld
CP-01Total position must decrease accordingly on closeHeld
CP-02Total position should not underflow on closeHeld
CP-03User position must decrease accordingly on closeHeld
CP-04Debt should never increase on closeHeld
CP-05Close should never decrease user reserve balancesHeld
CP-FAILclosePosition should not revert with Insolvent error if not in floorBroken
DROP-01Anchor tick matches sweep tick post-dropHeld
DROP-02Anchor lower tick stays the same post-dropHeld
DROP-03Floor range stays the same post-dropHeld
DROP-04Discovery upper and lower ticks should not be greater than their prior ticks.Held
DROP-05Discovery range size should not changeHeld
DROP-06Anchor liquidity stays the same post-dropHeld
DROP-07Floor reserves should not decrease post-drop within deltaHeld
DROP-08Floor capacity should not decrease post-drop within deltaHeld
DROP-09Floor liquidity should not decrease post-drop within deltaHeld
DROP-10Reserves in the anchor position should be the same (or within 1 wei) before and afterHeld
LF-01a drop call System should be solvent after charging fundingBroken
EXTEND-01newDurationDays_ and accountBefore.expiry must be valid forHeld
EXTEND-02successful extend New expiry return data after borrow is called should match creditors accountHeld
EXTEND-03details Expiry must be in the futureHeld
EXTEND-04Expiry should increase after extensionHeld
EXTEND-05Credit can't be higher than collateral - beforeHeld
EXTEND-06Credit can't be higher than collateral - afterHeld
EXTEND-07BPOOL should hold no reserve in its balance within deltaHeld
MM-05Should never be able to sweep and slide at the same timeHeld
MM-06slideTick should always be less than or equal to the sweepTickHeld

More from Baseline Markets

All 12 reports
  1. Mercury, Round 3

    109 findings4 critical · 9 high 109 findings: 4 critical, 9 high, 28 medium, 33 low, 35 informational
  2. AMM, Round 2

    47 findings4 critical · 14 high 47 findings: 4 critical, 14 high, 8 medium, 13 low, 8 informational
  3. AMM

    54 findings3 critical · 6 high 54 findings: 3 critical, 6 high, 13 medium, 11 low, 21 informational
  4. 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.

Get a quote