Skip to main content
Liquity v2 (second audit) - Nov 11, 2024

Liquity v2

(second audit)

Smart Contract Security Assessment

November 11, 2024

Liquity

SUMMARY


ABSTRACT

Dedaub was commissioned to perform a security audit of the Liquity v2 (BOLD) protocol, focusing on both the smart contract code and the overall logic of the system. The codebase was of high quality, with extensive comments and a detailed README that thoroughly describes all the aspects of the protocol. Additionally, the protocol is supported by an extensive test suite.


BACKGROUND

Liquity v2 is a decentralized lending protocol that mints a stablecoin called BOLD. The core mechanics are the same as in Liquity v1: users deposit collateral, and the protocol mints BOLD tokens. As long as a user's collateral ratio stays above a certain threshold, they remain safe from liquidation. However, if their collateral ratio drops below this threshold, anyone can trigger the liquidation process. The debt and collateral from a liquidated trove are absorbed by the stability pool, with any excess distributed proportionally to other troves based on their collateral. BOLD is redeemable at its face value of $1. It’s important to note that while Liquity v2 shares similarities with v1, the two protocols are entirely independent.

Key Differences and New Features in Liquity v2:

  1. Multi-Collateral Support: Liquity v2 introduces support for multiple collateral types, including wrapped ETH and various LSTs. Each collateral type forms its own branch with a separate set of troves and a dedicated stability pool.
  2. Interest Rate Mechanism: Instead of paying a one-time fee when opening a trove, users in Liquity v2 select an annual interest rate, which they will pay over time. The interest is split between stability pool depositors and other liquidity providers, such as AMM LPs for pools containing BOLD. The percentage of interest allocated to the stability pool is fixed at deployment and cannot be changed.
  3. Redemption Process: When redeeming BOLD tokens, users cannot choose which collateral they will receive. Instead, redemptions are distributed across the various collateral branches in proportion to their unbacked BOLD (i.e., total BOLD minted by the branch minus the amount of BOLD held in the branch's stability pool). Troves within each branch are ordered by their annual interest rate, with redemptions first targeting troves with lower interest rates. To avoid being targeted by redemptions, trove owners must adjust their interest rates. The protocol simplifies this process by allowing batch interest delegation. Any user can open an interest manager, charge a fee, and let trove owners join the manager to have their interest rates managed and updated.

SETTING & CAVEATS

This audit report covers the contracts of the repository https://github.com/liquity/bold of the Liquity v2 protocol at commit 7b117762fb92f28f3accae6dabcc3d481765f27c. This audit mainly examines the updates to the codebase after the initial audit performed by Dedaub.

2 auditors worked on the changes to the codebase for 1 week on the following contracts:

src/
  • ActivePool.sol
  • AddressesRegistry.sol
  • BoldToken.sol
  • BorrowerOperations.sol
  • CollateralRegistry.sol
  • CollSurplusPool.sol
  • DefaultPool.sol
  • Dependencies/
    • AddRemoveManagers.sol
    • AggregatorV3Interface.sol
    • Constants.sol
    • IOsTokenVaultController.sol
    • IStaderOracle.sol
    • LiquityBase.sol
    • LiquityMath.sol
    • Ownable.sol
  • GasPool.sol
  • HintHelpers.sol
  • MultiTroveGetter.sol
  • PriceFeeds/
    • CompositePriceFeed.sol
    • MainnetPriceFeedBase.sol
    • RETHPriceFeed.sol
    • WETHPriceFeed.sol
    • WSTETHPriceFeed.sol
  • SortedTroves.sol
  • StabilityPool.sol
  • TroveManager.sol
  • TroveNFT.sol
  • Types/
    • BatchId.sol
    • LatestBatchData.sol
    • LatestTroveData.sol
    • TroveChange.sol
    • TroveId.sol
  • Zappers/
    • BaseZapper.sol
    • GasCompZapper.sol
    • Interfaces/
      • LeftoversSweep.sol
      • LeverageLSTZapper.sol
      • LeverageWETHZapper.sol
      • Modules/
        • WETHZapper.sol

The audit’s main target is security threats, i.e., what the community understanding would likely call "hacking", rather than the regular use of the protocol. Functional correctness (i.e. issues in "regular use") is a secondary consideration. Typically it can only be covered if we are provided with unambiguous (i.e. full-detail) specifications of what is the expected, correct behavior. In terms of functional correctness, we often trusted the code’s calculations and interactions, in the absence of any other specification. Functional correctness relative to low-level calculations (including units, scaling and quantities returned from external protocols) is generally most effectively done through thorough testing rather than human auditing.


VULNERABILITIES & FUNCTIONAL ISSUES

This section details issues affecting the functionality of the contract. Dedaub generally categorizes issues according to the following severities, but may also take other considerations into account such as impact or difficulty in exploitation:

Category
Description
CRITICAL
Can be profitably exploited by any knowledgeable third-party attacker to drain a portion of the system’s or users’ funds OR the contract does not function as intended and severe loss of funds may result.
HIGH
Third-party attackers or faulty functionality may block the system or cause the system or users to lose funds. Important system invariants can be violated.
MEDIUM
Examples:
  • User or system funds can be lost when third-party systems misbehave.
  • DoS, under specific conditions.
  • Part of the functionality becomes unusable due to a programming error.
LOW
Examples:
  • Breaking important system invariants but without apparent consequences.
  • Buggy functionality for trusted users where a workaround exists.
  • Security issues which may manifest when the system evolves.

Issue resolution includes “dismissed” or “acknowledged” but no action taken, by the client, or “resolved”, per the auditors.


CRITICAL SEVERITY

[No critical severity issues]


HIGH SEVERITY

[No high severity issues]


MEDIUM SEVERITY

[No medium severity issues]


LOW SEVERITY

L1

The WETHPriceFeed constructor does not actually verify that its oracle setup is operating

LOW
resolved
L1
The WETHPriceFeed constructor does not actually verify that its oracle setup is operating

In the WETHPriceFeed::constructor, the existing call to the function _fetchPricePrimary(boolean) is essentially a no-op, due to it being empty. Thus, no attempt to verify that the setup is correct and the oracle is working appropriately is made, which is not the expected behavior. WETHPriceFeed::_fetchPricePrimary(), which has no arguments, should be called instead or _fetchPricePrimary(boolean) should be changed to call _fetchPricePrimary().

L2

A zombie trove with ICR < 100% can become permanently unredeemable

LOW
acknowledged
L2
A zombie trove with ICR < 100% can become permanently unredeemable

In TroveManager::redeemCollateral, if there exists a pointer to a zombie trove but for that trove it is true that getCurrentICR(singleRedemption.troveId, _price) < _100pct, then the trove is skipped for the current redeem call. If this redeem call creates a new zombie, the reference to the original zombie with ICR < 100pct is lost, as the pointer variable will now point to the newly created zombie. In the unlikely case that the original zombie does not get liquidated, it will remain unredeemable.

L3

A ShutDownFromOracleFailure event is emitted when execution reverts due to an exchange rate failure

LOW
resolved
L3
A ShutDownFromOracleFailure event is emitted when execution reverts due to an exchange rate failure

In RETHPriceFeed::_fetchPricePrimary (line 44), the shutdown function _shutDownAndSwitchToLastGoodPrice emits a ShutDownFromOracleFailure event indicating an ethUSD oracle failure.

RETHPriceFeed::_fetchPricePrimary():35-76
function _fetchPricePrimary(bool _isRedemption) internal override
returns (uint256, bool)
{
assert(priceSource == PriceSource.primary);
(uint256 ethUsdPrice, bool ethUsdOracleDown) =
_getOracleAnswer(ethUsdOracle);
(uint256 rEthEthPrice, bool rEthEthOracleDown) =
_getOracleAnswer(rEthEthOracle);
(uint256 rEthPerEth, bool exchangeRateIsDown) = _getCanonicalRate();

if (ethUsdOracleDown || exchangeRateIsDown) {
// Dedaub: the _shutDownAndSwitchToLastGoodPrice call will emit a
// ShutDownFromOracleFailure event even though the failure might
// be related to the LST exchange rate.
return (
_shutDownAndSwitchToLastGoodPrice(
address(ethUsdOracle.aggregator)
),
true
);
}

// Dedaub: code omitted for brevity
}

However, the ShutDownFromOracleFailure event will be emitted not only when the oracle fails but also when the LST exchange rate call fails, making it less informative for pinpointing the exact cause. The event could be renamed to something generic, i.e. "ShutDownFromFailedExternalCall", or extra logic could be added to emit more specific events based on whether an oracle or an LST exchange rate calculation failed.

L4

HybridCurveUniV3Exchange::swapFromBold uses incorrect fee when the collateral is WETH

LOW
resolved
L4
HybridCurveUniV3Exchange::swapFromBold uses incorrect fee when the collateral is WETH

The function HybridCurveUniV3Exchange::swapFromBold compiles the swap path with the incorrect fee amount in the case where address(WETH) == address(collToken). Precisely, feeWethColl is used instead of feeUsdcWeth.



OTHER / ADVISORY ISSUES

This section details issues that are not thought to directly affect the functionality of the project, but we recommend considering them.

A1

No-op storage updates

ADVISORY
resolved
A1
No-op storage updates

In StabilityPool::_computeCollRewardsPerUnitStaked, if _debtToOffset == _totalBoldDeposits the error-tracking variables lastBoldLossError_Offset and lastBoldLossError_TotalDeposits are updated but it seems that these updates are essentially no-ops, since later in the execution these variables will be assigned new values ​​by _updateCollRewardSumAndProduct.

A2

Unused interfaces

ADVISORY
info
A2
Unused interfaces

The following interfaces are not used:

  • Dependencies/IOsTokenVaultController.sol
  • Dependencies/IStaderOracle.sol

A3

Compiler bugs

ADVISORY
info
A3
Compiler bugs

The code is compiled with Solidity 0.8.24. Version 0.8.24, in particular, has no known bugs.



DISCLAIMER

The audited contracts have been analyzed using automated techniques and extensive human inspection in accordance with state-of-the-art practices as of the date of this report. The audit makes no statements or warranties on the security of the code. On its own, it cannot be considered a sufficient assessment of the correctness of the contract. While we have conducted an analysis to the best of our ability, it is our recommendation for high-value contracts to commission several independent audits, a public bug bounty program, as well as continuous security auditing and monitoring through Dedaub Security Suite.


ABOUT DEDAUB

Dedaub offers significant security expertise combined with cutting-edge program analysis technology to secure some of the most prominent protocols in DeFi. The founders, as well as many of Dedaub's auditors, have a strong academic research background together with a real-world hacker mentality to secure code. Protocol blockchain developers hire us for our foundational analysis tools and deep expertise in program analysis, reverse engineering, DeFi exploits, cryptography and financial mathematics.