Liquity v2
(second audit)
Smart Contract Security Assessment
November 11, 2024
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:
- 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.
- 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.
- 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:
- 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:
- 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.
- 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
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()
.
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
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.
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.
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
.
The following interfaces are not used:
- Dependencies/IOsTokenVaultController.sol
- Dependencies/IStaderOracle.sol
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.