From 6cb2710d23ce97f0be2806f6a4f8598a3cabc85b Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Sat, 14 Feb 2026 17:03:34 -0300 Subject: [PATCH 1/9] feat: stop using auction for liquidations --- script/interfaces/ICatFactory.sol | 30 +- src/auction.vy | 63 +- src/dutch_desk.vy | 80 +- src/factory.vy | 17 +- src/interfaces/IAuction.vyi | 18 +- src/interfaces/IDutchDesk.vyi | 10 +- src/interfaces/ILenderFactory.vyi | 8 +- src/interfaces/ITaker.vyi | 4 +- src/lender/Lender.sol | 21 +- src/lender/LenderFactory.sol | 4 +- src/lender/interfaces/IAuction.sol | 8 - src/lender/interfaces/ILender.sol | 1 - src/trove_manager.vy | 143 +-- test/Auction.t.sol | 169 +-- test/AuctionPriceSimulation.t.sol | 4 +- test/Base.sol | 38 +- test/Borrow.t.sol | 7 +- test/DutchDesk.t.sol | 115 +- test/Gas.t.sol | 60 +- test/LaggingOracleValueExtractionPOC.t.sol | 4 +- test/Lend.t.sol | 163 --- test/Liquidate.t.sol | 1332 ++++++++++---------- test/OpenTrove.t.sol | 28 +- test/TransferOwnership.t.sol | 1 + test/TroveManager.t.sol | 18 +- test/interfaces/IAuction.sol | 32 +- test/interfaces/IDutchDesk.sol | 27 +- test/interfaces/ITroveManager.sol | 27 +- 28 files changed, 1017 insertions(+), 1415 deletions(-) delete mode 100644 src/lender/interfaces/IAuction.sol diff --git a/script/interfaces/ICatFactory.sol b/script/interfaces/ICatFactory.sol index b024085..f0339d9 100644 --- a/script/interfaces/ICatFactory.sol +++ b/script/interfaces/ICatFactory.sol @@ -8,23 +8,21 @@ interface ICatFactory { // ============================================================================================ struct DeployParams { - address borrowToken; - address collateralToken; - address priceOracle; + address borrow_token; + address collateral_token; + address price_oracle; address management; - address performanceFeeRecipient; - uint256 minimumDebt; - uint256 minimumCollateralRatio; - uint256 upfrontInterestPeriod; - uint256 interestRateAdjCooldown; - uint256 redemptionMinimumPriceBufferPercentage; - uint256 redemptionStartingPriceBufferPercentage; - uint256 redemptionReKickStartingPriceBufferPercentage; - uint256 liquidationMinimumPriceBufferPercentage; - uint256 liquidationStartingPriceBufferPercentage; - uint256 stepDuration; - uint256 stepDecayRate; - uint256 auctionLength; + address performance_fee_recipient; + uint256 minimum_debt; + uint256 minimum_collateral_ratio; + uint256 upfront_interest_period; + uint256 interest_rate_adj_cooldown; + uint256 minimum_price_buffer_percentage; + uint256 starting_price_buffer_percentage; + uint256 re_kick_starting_price_buffer_percentage; + uint256 step_duration; + uint256 step_decay_rate; + uint256 auction_length; bytes32 salt; } diff --git a/src/auction.vy b/src/auction.vy index 57546a9..4b53864 100644 --- a/src/auction.vy +++ b/src/auction.vy @@ -50,7 +50,6 @@ struct AuctionInfo: minimum_price: uint256 receiver: address surplus_receiver: address - is_liquidation: bool struct InitializeParams: @@ -94,7 +93,6 @@ step_decay_rate: public(uint256) auction_length: public(uint256) # Accounting -liquidation_auctions: public(uint256) # count of active liquidation auctions auctions: public(HashMap[uint256, AuctionInfo]) # auction ID --> AuctionInfo @@ -236,16 +234,6 @@ def is_active(auction_id: uint256) -> bool: return self._is_active(auction) -@external -@view -def is_ongoing_liquidation_auction() -> bool: - """ - @notice Check if there's at least one ongoing liquidation auction - @return Whether there's at least one ongoing liquidation auction - """ - return self.liquidation_auctions > 0 - - # ============================================================================================ # Kick # ============================================================================================ @@ -260,7 +248,6 @@ def kick( minimum_price: uint256, receiver: address, surplus_receiver: address, - is_liquidation: bool, ): """ @notice Kick off an auction @@ -273,7 +260,6 @@ def kick( @param minimum_price The minimum price for the auction, WAD scaled in buy token @param receiver The address that will receive the auction proceeds @param surplus_receiver The address that will receive any surplus proceeds above maximum_amount - @param is_liquidation Whether this auction is selling liquidated collateral """ # Make sure caller is Papi assert msg.sender == self.papi, "!papi" @@ -299,10 +285,6 @@ def kick( # Make sure auction is not already active assert not self._is_active(auction), "active" - # If liquidation auction, increment counter - if is_liquidation: - self.liquidation_auctions += 1 - # Update storage self.auctions[auction_id] = AuctionInfo( kick_timestamp=block.timestamp, @@ -314,7 +296,6 @@ def kick( minimum_price=minimum_price, receiver=receiver, surplus_receiver=surplus_receiver, - is_liquidation=is_liquidation, ) # Pull the tokens from Papi @@ -417,16 +398,12 @@ def take( # Reset kick timestamp to mark auction as inactive auction.kick_timestamp = 0 - # If it was a liquidation auction, decrement counter - if auction.is_liquidation: - self.liquidation_auctions -= 1 - # Send the token being sold to the take receiver assert extcall self.sell_token.transfer(receiver, take_amount, default_return_value=True) # If the caller provided data, perform the callback if len(data) != 0: - extcall ITaker(receiver).auctionTakeCallback( + extcall ITaker(receiver).takeCallback( auction_id, msg.sender, take_amount, @@ -437,30 +414,24 @@ def take( # Cache the buy token contract buy_token: IERC20 = self.buy_token - # If liquidation auction, all proceeds goes to the Lender contract. - # Otherwise, make sure the receiver does not get more than the maximum and transfer the surplus to the surplus receiver - if auction.is_liquidation: - # Liquidation: all to the Lender contract + # How much the receiver still needs + receiver_remaining: uint256 = auction.maximum_amount - auction.amount_received + + # If the bought amount is less than the receiver's maximum amount, transfer him all of it. + # Otherwise, cover the receiver first, then transfer the surplus to the surplus receiver + if needed_amount <= receiver_remaining: + # Entire amount to the receiver + auction.amount_received += needed_amount assert extcall buy_token.transferFrom(msg.sender, auction.receiver, needed_amount, default_return_value=True) else: - # How much the receiver still needs - receiver_remaining: uint256 = auction.maximum_amount - auction.amount_received - - # If the bought amount is less than the receiver's maximum amount, transfer him all of it. - # Otherwise, cover the receiver first, then transfer the surplus to the surplus receiver - if needed_amount <= receiver_remaining: - # Entire amount to the receiver - auction.amount_received += needed_amount - assert extcall buy_token.transferFrom(msg.sender, auction.receiver, needed_amount, default_return_value=True) - else: - # Cover the receiver first - if receiver_remaining > 0: - auction.amount_received = auction.maximum_amount - assert extcall buy_token.transferFrom(msg.sender, auction.receiver, receiver_remaining, default_return_value=True) - - # Transfer the surplus to the Lender contract - surplus: uint256 = needed_amount - receiver_remaining - assert extcall buy_token.transferFrom(msg.sender, auction.surplus_receiver, surplus, default_return_value=True) + # Cover the receiver first + if receiver_remaining > 0: + auction.amount_received = auction.maximum_amount + assert extcall buy_token.transferFrom(msg.sender, auction.receiver, receiver_remaining, default_return_value=True) + + # Transfer the surplus to the surplus receiver + surplus: uint256 = needed_amount - receiver_remaining + assert extcall buy_token.transferFrom(msg.sender, auction.surplus_receiver, surplus, default_return_value=True) # Update storage. No need to worry about re-entrancy since non-reentrant pragma is enabled self.auctions[auction_id] = auction diff --git a/src/dutch_desk.vy b/src/dutch_desk.vy index 9f84353..550719d 100644 --- a/src/dutch_desk.vy +++ b/src/dutch_desk.vy @@ -4,7 +4,7 @@ @title Dutch Desk @license MIT @author Flex -@notice Handles liquidations and redemptions through Dutch Auctions +@notice Handles redemptions through Dutch Auctions """ from ethereum.ercs import IERC20 @@ -38,11 +38,9 @@ collateral_token: public(IERC20) collateral_token_precision: public(uint256) # Parameters -redemption_minimum_price_buffer_percentage: public(uint256) -redemption_starting_price_buffer_percentage: public(uint256) -redemption_re_kick_starting_price_buffer_percentage: public(uint256) -liquidation_minimum_price_buffer_percentage: public(uint256) -liquidation_starting_price_buffer_percentage: public(uint256) +minimum_price_buffer_percentage: public(uint256) +starting_price_buffer_percentage: public(uint256) +re_kick_starting_price_buffer_percentage: public(uint256) # Accounting nonce: public(uint256) @@ -60,11 +58,9 @@ struct InitializeParams: auction: address borrow_token: address collateral_token: address - redemption_minimum_price_buffer_percentage: uint256 - redemption_starting_price_buffer_percentage: uint256 - redemption_re_kick_starting_price_buffer_percentage: uint256 - liquidation_minimum_price_buffer_percentage: uint256 - liquidation_starting_price_buffer_percentage: uint256 + minimum_price_buffer_percentage: uint256 + starting_price_buffer_percentage: uint256 + re_kick_starting_price_buffer_percentage: uint256 # ============================================================================================ @@ -91,11 +87,9 @@ def initialize(params: InitializeParams): self.collateral_token = IERC20(params.collateral_token) # Set parameters - self.redemption_minimum_price_buffer_percentage = params.redemption_minimum_price_buffer_percentage - self.redemption_starting_price_buffer_percentage = params.redemption_starting_price_buffer_percentage - self.redemption_re_kick_starting_price_buffer_percentage = params.redemption_re_kick_starting_price_buffer_percentage - self.liquidation_minimum_price_buffer_percentage = params.liquidation_minimum_price_buffer_percentage - self.liquidation_starting_price_buffer_percentage = params.liquidation_starting_price_buffer_percentage + self.minimum_price_buffer_percentage = params.minimum_price_buffer_percentage + self.starting_price_buffer_percentage = params.starting_price_buffer_percentage + self.re_kick_starting_price_buffer_percentage = params.re_kick_starting_price_buffer_percentage # Get collateral token decimals collateral_token_decimals: uint256 = convert(staticcall IERC20Detailed(params.collateral_token).decimals(), uint256) @@ -113,21 +107,14 @@ def initialize(params: InitializeParams): @external -def kick( - kick_amount: uint256, - maximum_amount: uint256 = 0, - receiver: address = empty(address), - is_liquidation: bool = True, -): +def kick(kick_amount: uint256, maximum_amount: uint256, receiver: address): """ @notice Kicks an auction of collateral tokens for borrow tokens @dev Only callable by the Trove Manager contract - @dev Will use the Lender contract as receiver of auction proceeds if `receiver` is zero address @dev Caller must approve this contract to transfer collateral tokens on its behalf before calling @param kick_amount Amount of collateral tokens to auction @param maximum_amount The maximum amount borrow tokens to be received @param receiver Address to receive the auction proceeds in borrow tokens - @param is_liquidation Whether this auction is for liquidated collateral """ # Make sure caller is the Trove Manager contract assert msg.sender == self.trove_manager, "!trove_manager" @@ -139,7 +126,10 @@ def kick( # Get the starting and minimum prices starting_price: uint256 = 0 minimum_price: uint256 = 0 - starting_price, minimum_price = self._get_prices(kick_amount, is_liquidation) + starting_price, minimum_price = self._get_prices( + kick_amount, + self.starting_price_buffer_percentage, + ) # Use the nonce as auction identifier auction_id: uint256 = self.nonce @@ -147,7 +137,7 @@ def kick( # Increment the nonce self.nonce = auction_id + 1 - # Pull the collateral tokens from the Trove Manager + # Pull the collateral tokens from the Trove Manager contract assert extcall self.collateral_token.transferFrom(self.trove_manager, self, kick_amount, default_return_value=True) # Kick the auction @@ -157,9 +147,8 @@ def kick( maximum_amount, starting_price, minimum_price, - receiver if receiver != empty(address) else self.lender, + receiver, self.lender, # surplus receiver - is_liquidation, ) @@ -169,23 +158,22 @@ def re_kick(auction_id: uint256): @notice Re-kick an inactive auction with new starting and minimum prices @dev Will revert if the auction is not kickable @dev An auction may need to be re-kicked if its price has fallen below its minimum price - @dev Uses a higher starting price buffer percentage to allow for takers to regroup + @dev May use a higher starting price buffer percentage to allow for takers to regroup @dev Does not set the receiver nor transfer collateral as those are already ready in the auction @param auction_id Identifier of the auction to re-kick """ # Cache the Auction contract auction: IAuction = self.auction - # Cache the auction info - auction_info: IAuction.AuctionInfo = staticcall auction.auctions(auction_id) + # Get the auction info + auction_info: IAuction.AuctionInfo = staticcall self.auction.auctions(auction_id) # Get new starting and minimum prices starting_price: uint256 = 0 minimum_price: uint256 = 0 starting_price, minimum_price = self._get_prices( - auction_info.currentAmount, - auction_info.isLiquidation, - True, # is_rekick + auction_info.current_amount, + self.re_kick_starting_price_buffer_percentage, ) # Re-kick with new prices @@ -199,32 +187,14 @@ def re_kick(auction_id: uint256): @internal @view -def _get_prices( - kick_amount: uint256, - is_liquidation: bool, - is_rekick: bool = False, -) -> (uint256, uint256): +def _get_prices(kick_amount: uint256, starting_price_buffer_pct: uint256) -> (uint256, uint256): """ @notice Gets the starting and minimum prices for an auction @param kick_amount Amount of collateral tokens to auction - @param is_liquidation Whether this is a liquidation auction - @param is_rekick Whether this is a re-kick of an existing auction + @param starting_price_buffer_pct The buffer percentage to apply to the starting price @return starting_price The calculated starting price @return minimum_price The calculated minimum price """ - # Determine the minimum and starting price buffer percentages - minimum_price_buffer_pct: uint256 = 0 - starting_price_buffer_pct: uint256 = 0 - if is_liquidation: - minimum_price_buffer_pct = self.liquidation_minimum_price_buffer_percentage - starting_price_buffer_pct = self.liquidation_starting_price_buffer_percentage - else: - minimum_price_buffer_pct = self.redemption_minimum_price_buffer_percentage - if is_rekick: - starting_price_buffer_pct = self.redemption_re_kick_starting_price_buffer_percentage - else: - starting_price_buffer_pct = self.redemption_starting_price_buffer_percentage - # Get the collateral price collateral_price: uint256 = staticcall self.price_oracle.get_price(False) # price in WAD format @@ -234,6 +204,6 @@ def _get_prices( # Calculate the minimum price with buffer to the collateral price # Minimum price is per token and is scaled to WAD - minimum_price: uint256 = collateral_price * minimum_price_buffer_pct // _WAD + minimum_price: uint256 = collateral_price * self.minimum_price_buffer_percentage // _WAD return starting_price, minimum_price \ No newline at end of file diff --git a/src/factory.vy b/src/factory.vy index dbdf6ae..02e4396 100644 --- a/src/factory.vy +++ b/src/factory.vy @@ -45,11 +45,9 @@ struct DeployParams: minimum_collateral_ratio: uint256 # minimum CR to avoid liquidation, e.g., `110 * one_pct` for 110% upfront_interest_period: uint256 # duration for upfront interest charges, e.g., `7 * 24 * 60 * 60` for 7 days interest_rate_adj_cooldown: uint256 # cooldown between rate adjustments, e.g., `7 * 24 * 60 * 60` for 7 days - redemption_minimum_price_buffer_percentage: uint256 # redemption auction minimum price buffer, e.g. `WAD - 5 * 10 ** 16` for 5% below oracle price - redemption_starting_price_buffer_percentage: uint256 # redemption auction starting price buffer, e.g. `WAD + 1 * 10 ** 16` for 1% above oracle price. must be >= max oracle deviation from market price to ensure the starting auction price is always above market price, preventing value extraction from oracle lag - redemption_re_kick_starting_price_buffer_percentage: uint256 # redemption auction re-kick price buffer, e.g. `WAD + 5 * 10 ** 16` for 5% above oracle price - liquidation_minimum_price_buffer_percentage: uint256 # liquidation auction minimum price buffer, e.g. `WAD - 10 * 10 ** 16` for 10% below oracle price - liquidation_starting_price_buffer_percentage: uint256 # liquidation auction starting price buffer, e.g., `WAD - 1 * 10 ** 16` for 1% below oracle price + minimum_price_buffer_percentage: uint256 # auction minimum price buffer, e.g. `WAD - 5 * 10 ** 16` for 5% below oracle price + starting_price_buffer_percentage: uint256 # auction starting price buffer, e.g. `WAD + 1 * 10 ** 16` for 1% above oracle price. must be >= max oracle deviation from market price to ensure the starting auction price is always above market price, preventing value extraction from oracle lag + re_kick_starting_price_buffer_percentage: uint256 # auction re-kick price buffer, e.g. `WAD + 5 * 10 ** 16` for 5% above oracle price step_duration: uint256 # duration of each price step, e.g., `60` for price change every minute step_decay_rate: uint256 # decay rate per step, e.g., `50` for 0.5% decrease per step auction_length: uint256 # total auction duration in seconds, e.g., `86400` for 1 day @@ -154,7 +152,6 @@ def deploy(params: DeployParams) -> (address, address, address, address, address # Deploy the Lender contract via the Lender Factory lender: address = extcall LENDER_FACTORY.deploy( params.borrow_token, - auction, trove_manager, params.management, params.performance_fee_recipient, @@ -186,11 +183,9 @@ def deploy(params: DeployParams) -> (address, address, address, address, address auction=auction, borrow_token=params.borrow_token, collateral_token=params.collateral_token, - redemption_minimum_price_buffer_percentage=params.redemption_minimum_price_buffer_percentage, - redemption_starting_price_buffer_percentage=params.redemption_starting_price_buffer_percentage, - redemption_re_kick_starting_price_buffer_percentage=params.redemption_re_kick_starting_price_buffer_percentage, - liquidation_minimum_price_buffer_percentage=params.liquidation_minimum_price_buffer_percentage, - liquidation_starting_price_buffer_percentage=params.liquidation_starting_price_buffer_percentage, + minimum_price_buffer_percentage=params.minimum_price_buffer_percentage, + starting_price_buffer_percentage=params.starting_price_buffer_percentage, + re_kick_starting_price_buffer_percentage=params.re_kick_starting_price_buffer_percentage, )) # Initialize the Auction contract diff --git a/src/interfaces/IAuction.vyi b/src/interfaces/IAuction.vyi index 9d27dfd..998d5a5 100644 --- a/src/interfaces/IAuction.vyi +++ b/src/interfaces/IAuction.vyi @@ -7,16 +7,15 @@ struct AuctionInfo: - kickTimestamp: uint256 - initialAmount: uint256 - currentAmount: uint256 - maximumAmount: uint256 - amountReceived: uint256 - startingPrice: uint256 - minimumPrice: uint256 + kick_timestamp: uint256 + initial_amount: uint256 + current_amount: uint256 + maximum_amount: uint256 + amount_received: uint256 + starting_price: uint256 + minimum_price: uint256 receiver: address - surplusReceiver: address - isLiquidation: bool + surplus_receiver: address struct InitializeParams: @@ -63,7 +62,6 @@ def kick( minimum_price: uint256, receiver: address, surplus_receiver: address, - is_liquidation: bool, ): ... diff --git a/src/interfaces/IDutchDesk.vyi b/src/interfaces/IDutchDesk.vyi index 18f0e60..3b56164 100644 --- a/src/interfaces/IDutchDesk.vyi +++ b/src/interfaces/IDutchDesk.vyi @@ -13,11 +13,9 @@ struct InitializeParams: auction: address borrow_token: address collateral_token: address - redemption_minimum_price_buffer_percentage: uint256 - redemption_starting_price_buffer_percentage: uint256 - redemption_re_kick_starting_price_buffer_percentage: uint256 - liquidation_minimum_price_buffer_percentage: uint256 - liquidation_starting_price_buffer_percentage: uint256 + minimum_price_buffer_percentage: uint256 + starting_price_buffer_percentage: uint256 + re_kick_starting_price_buffer_percentage: uint256 # ============================================================================================ @@ -36,5 +34,5 @@ def initialize(params: InitializeParams): @external -def kick(amount: uint256, maximum_amount: uint256 = 0, receiver: address = empty(address), is_liquidation: bool = True): +def kick(amount: uint256, maximum_amount: uint256, receiver: address): ... \ No newline at end of file diff --git a/src/interfaces/ILenderFactory.vyi b/src/interfaces/ILenderFactory.vyi index 5e60fe7..827bec7 100644 --- a/src/interfaces/ILenderFactory.vyi +++ b/src/interfaces/ILenderFactory.vyi @@ -7,5 +7,11 @@ @external -def deploy(_asset: address, _auction: address, _troveManager: address, _management: address, _performanceFeeRecipient: address, name: String[77]) -> address: +def deploy( + _asset: address, + _troveManager: address, + _management: address, + _performanceFeeRecipient: address, + name: String[77], +) -> address: ... \ No newline at end of file diff --git a/src/interfaces/ITaker.vyi b/src/interfaces/ITaker.vyi index cc8811f..a1b75f0 100644 --- a/src/interfaces/ITaker.vyi +++ b/src/interfaces/ITaker.vyi @@ -2,11 +2,11 @@ @external -def auctionTakeCallback( +def takeCallback( auction_id: uint256, taker: address, amount_taken: uint256, - want_amount: uint256, + needed_amount: uint256, data: Bytes[10**5], ): ... \ No newline at end of file diff --git a/src/lender/Lender.sol b/src/lender/Lender.sol index 4bda8aa..ae8ab1a 100644 --- a/src/lender/Lender.sol +++ b/src/lender/Lender.sol @@ -5,7 +5,6 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {BaseHooks, ERC20} from "@periphery/Bases/Hooks/BaseHooks.sol"; import {BaseStrategy} from "@tokenized-strategy/BaseStrategy.sol"; -import {IAuction} from "./interfaces/IAuction.sol"; import {ITroveManager} from "./interfaces/ITroveManager.sol"; /// @title Lender @@ -27,9 +26,6 @@ contract Lender is BaseHooks { // Constants // ============================================================================================ - /// @notice Auction contract - IAuction public immutable AUCTION; - /// @notice TroveManager contract ITroveManager public immutable TROVE_MANAGER; @@ -50,17 +46,14 @@ contract Lender is BaseHooks { /// @notice Constructor /// @param _asset The address of the borrow token - /// @param _auction The address of the Auction contract /// @param _troveManager The address of the TroveManager contract /// @param _name The name of the vault constructor( address _asset, - address _auction, address _troveManager, string memory _name ) BaseHooks(_asset, _name) { - // Set immutable addresses - AUCTION = IAuction(_auction); + // Set Trove Manager contract TROVE_MANAGER = ITroveManager(_troveManager); // No deposit limit by default @@ -81,18 +74,6 @@ contract Lender is BaseHooks { return _depositLimit <= _currentAssets ? 0 : _depositLimit - _currentAssets; } - /// @inheritdoc BaseStrategy - function availableWithdrawLimit(address /*_owner*/) public view override returns (uint256) { - // If the strategy is shutdown always allow full withdrawals - if (TokenizedStrategy.isShutdown()) return type(uint256).max; - - // Withdrawals are blocked during ongoing liquidation auctions. - // During liquidation, collateral has been seized but not yet sold for borrow tokens. - // The system is temporarily insolvent until the auction completes and proceeds return to this contract. - // However, any idle liquidity already in the contract remains available for withdrawal - return AUCTION.is_ongoing_liquidation_auction() ? asset.balanceOf(address(this)) : type(uint256).max; - } - // ============================================================================================ // Management functions // ============================================================================================ diff --git a/src/lender/LenderFactory.sol b/src/lender/LenderFactory.sol index a489db2..ab292f8 100644 --- a/src/lender/LenderFactory.sol +++ b/src/lender/LenderFactory.sol @@ -23,7 +23,6 @@ contract LenderFactory { /// @notice Deploy a new Lender contract /// @param _asset The address of the borrow token - /// @param _auction The address of the Auction contract /// @param _troveManager The address of the Trove Manager contract /// @param _management The address of the management /// @param _performanceFeeRecipient The address of the performance fee recipient @@ -31,14 +30,13 @@ contract LenderFactory { /// @return The address of the newly deployed Lender contract function deploy( address _asset, - address _auction, address _troveManager, address _management, address _performanceFeeRecipient, string calldata _name ) external returns (address) { // Deploy the Lender contract - ILender _lender = ILender(address(new Lender(_asset, _auction, _troveManager, _name))); + ILender _lender = ILender(address(new Lender(_asset, _troveManager, _name))); // Set initial parameters _lender.setKeeper(KEEPER); diff --git a/src/lender/interfaces/IAuction.sol b/src/lender/interfaces/IAuction.sol deleted file mode 100644 index e93f4dd..0000000 --- a/src/lender/interfaces/IAuction.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.23; - -interface IAuction { - - function is_ongoing_liquidation_auction() external view returns (bool); - -} diff --git a/src/lender/interfaces/ILender.sol b/src/lender/interfaces/ILender.sol index 47327ff..01d8633 100644 --- a/src/lender/interfaces/ILender.sol +++ b/src/lender/interfaces/ILender.sol @@ -9,7 +9,6 @@ interface ILender is IStrategy { // Constants // ============================================================================================ - function AUCTION() external view returns (address); function TROVE_MANAGER() external view returns (address); // ============================================================================================ diff --git a/src/trove_manager.vy b/src/trove_manager.vy index 8466fe0..c594bcf 100644 --- a/src/trove_manager.vy +++ b/src/trove_manager.vy @@ -14,6 +14,7 @@ from ethereum.ercs import IERC20Detailed from snekmate.utils import math +from interfaces import ITaker from interfaces import IDutchDesk from interfaces import IPriceOracle from interfaces import ISortedTroves @@ -147,12 +148,11 @@ struct InitializeParams: # ============================================================================================ +_MAX_CALLBACK_DATA_SIZE: constant(uint256) = 10**5 _PRICE_ORACLE_PRECISION: constant(uint256) = 10 ** 36 _WAD: constant(uint256) = 10 ** 18 -_MAX_LIQUIDATIONS: constant(uint256) = 20 _MAX_REDEMPTIONS: constant(uint256) = 1000 _ONE_YEAR: constant(uint256) = 365 * 60 * 60 * 24 -_REDEMPTION_AUCTION: constant(bool) = False # ============================================================================================ @@ -943,81 +943,35 @@ def close_zombie_trove(trove_id: uint256): @external -def liquidate_troves(trove_ids: uint256[_MAX_LIQUIDATIONS]): +def liquidate_trove( + trove_id: uint256, + max_amount: uint256 = max_value(uint256), + receiver: address = msg.sender, + data: Bytes[_MAX_CALLBACK_DATA_SIZE] = empty(Bytes[_MAX_CALLBACK_DATA_SIZE]) +) -> uint256: """ - @notice Liquidate a list of unhealthy Troves + @notice Liquidate a single unhealthy Trove @dev Uses the Dutch Desk contract to auction off the collateral tokens - @param trove_ids List of unique identifiers of the unhealthy Troves + @param trove_id Unique identifier of the unhealthy Trove + @param max_amount The maximum amount of debt to liquidate. Defaults to max uint256 + @param receiver The address that will receive the collateral tokens being sold. Defaults to msg.sender + @param data The data to pass to the `receiver` callback. Defaults to empty + @return collateral_taken The amount of liquidated collateral tokens """ - # Make sure that first trove id is non-zero - assert trove_ids[0] != 0, "!trove_ids" - - # Cache the current zombie trove id to avoid multiple SLOADs inside `_liquidate_single_trove` - current_zombie_trove_id: uint256 = self.zombie_trove_id + # Make sure the trove ID is non-zero + assert trove_id != 0, "!trove_id" # Get the collateral price collateral_price: uint256 = staticcall self.price_oracle.get_price() - # Initialize variables to track total changes - total_collateral_to_decrease: uint256 = 0 - total_debt_to_decrease: uint256 = 0 - total_weighted_debt_to_decrease: uint256 = 0 - - # Initialize variables to capture individual trove liquidation results - # Initializing outside of the loop to save gas on memory expansion - collateral_to_decrease: uint256 = 0 - debt_to_decrease: uint256 = 0 - weighted_debt_decrease: uint256 = 0 - - # Iterate over the Troves and liquidate them one by one - for trove_id: uint256 in trove_ids: - if trove_id == 0: - break - - # Liquidate the Trove and get the changes - ( - collateral_to_decrease, - debt_to_decrease, - weighted_debt_decrease - ) = self._liquidate_single_trove(trove_id, current_zombie_trove_id, collateral_price) - - # Accumulate the total changes - total_collateral_to_decrease += collateral_to_decrease - total_debt_to_decrease += debt_to_decrease - total_weighted_debt_to_decrease += weighted_debt_decrease - - # Update the contract's recorded collateral balance - self.collateral_balance -= total_collateral_to_decrease - - # Accrue interest on the total debt and update accounting - self._accrue_interest_and_account_for_trove_change( - 0, # debt_increase - total_debt_to_decrease, # debt_decrease - 0, # weighted_debt_increase - total_weighted_debt_to_decrease, # weighted_debt_decrease - ) - - # Kick the auction. Proceeds will be sent to the Lender contract - extcall self.dutch_desk.kick(total_collateral_to_decrease) # pulls collateral tokens - - -@internal -def _liquidate_single_trove(trove_id: uint256, current_zombie_trove_id: uint256, collateral_price: uint256) -> (uint256, uint256, uint256): - """ - @notice Internal function to liquidate a single unhealthy Trove - @dev Does not update global accounting or handle token transfers - @param trove_id Unique identifier of the Trove - @param current_zombie_trove_id Current zombie trove id - @param collateral_price Current collateral price - @return collateral_to_decrease Amount of collateral to subtract from the total collateral - @return debt_to_decrease Amount of debt to subtract from the total debt - @return weighted_debt_decrease Amount of weighted debt to subtract from the total weighted debt - """ - # Cache Trove info + # Cache the Trove info trove: Trove = self.troves[trove_id] + # Cache if the Trove is active + is_active: bool = trove.status == Status.ACTIVE + # Make sure the Trove is active or zombie - assert trove.status == Status.ACTIVE or trove.status == Status.ZOMBIE, "!active or zombie" + assert is_active or trove.status == Status.ZOMBIE, "!active or zombie" # Get the Trove's debt after accruing interest trove_debt_after_interest: uint256 = self._get_trove_debt_after_interest(trove) @@ -1030,8 +984,11 @@ def _liquidate_single_trove(trove_id: uint256, current_zombie_trove_id: uint256, # Make sure the collateral ratio is below the minimum collateral ratio assert collateral_ratio < self.minimum_collateral_ratio, "!collateral_ratio" - # Cache the Trove's old info for global accounting - old_trove: Trove = trove + # Cache the Trove's info before changing it + trove_owner: address = trove.owner + collateral_to_decrease: uint256 = trove.collateral + debt_to_decrease: uint256 = trove_debt_after_interest + weighted_debt_to_decrease: uint256 = trove.debt * trove.annual_interest_rate # Delete all Trove info and mark it as liquidated trove = empty(Trove) @@ -1041,27 +998,51 @@ def _liquidate_single_trove(trove_id: uint256, current_zombie_trove_id: uint256, self.troves[trove_id] = trove # If Trove is the current zombie trove, reset the `zombie_trove_id` variable - if current_zombie_trove_id == trove_id: + if self.zombie_trove_id == trove_id: self.zombie_trove_id = 0 - # Remove from sorted list if it was active - if old_trove.status == Status.ACTIVE: + # Update the contract's recorded collateral balance + self.collateral_balance -= collateral_to_decrease + + # Accrue interest on the total debt and update accounting + self._accrue_interest_and_account_for_trove_change( + 0, # debt_increase + debt_to_decrease, # debt_decrease + 0, # weighted_debt_increase + weighted_debt_to_decrease, # weighted_debt_decrease + ) + + # Remove from sorted list if Trove was active + if is_active: extcall self.sorted_troves.remove(trove_id) + # Send the collateral tokens to the `receiver` + assert extcall self.collateral_token.transfer(receiver, collateral_to_decrease, default_return_value=True) + + # If the caller provided data, perform the callback + if len(data) != 0: + extcall ITaker(receiver).takeCallback( + trove_id, + msg.sender, + collateral_to_decrease, # amount_taken + debt_to_decrease, # needed_amount + data, + ) + + # Pull the borrow tokens from caller and transfer them to the Lender contract + assert extcall self.borrow_token.transferFrom(msg.sender, self.lender, debt_to_decrease, default_return_value=True) + # Emit event log LiquidateTrove( trove_id=trove_id, - trove_owner=old_trove.owner, + trove_owner=trove_owner, liquidator=msg.sender, - collateral_amount=old_trove.collateral, - debt_amount=trove_debt_after_interest, + collateral_amount=collateral_to_decrease, + debt_amount=debt_to_decrease, ) - return ( - old_trove.collateral, # collateral_to_decrease - trove_debt_after_interest, # debt_to_decrease - old_trove.debt * old_trove.annual_interest_rate # weighted_debt_decrease - ) + # Return the amount of liquidated collateral tokens + return collateral_to_decrease # ============================================================================================ @@ -1238,7 +1219,7 @@ def _redeem( # Kick the auction # Proceeds up to `total_debt_decrease` will be sent to the `receiver`, any surplus will be sent to the Lender contract - extcall self.dutch_desk.kick(total_collateral_decrease, total_debt_decrease, receiver, _REDEMPTION_AUCTION) # pulls collateral tokens + extcall self.dutch_desk.kick(total_collateral_decrease, total_debt_decrease, receiver) # pulls collateral tokens # Emit event log Redeem( diff --git a/test/Auction.t.sol b/test/Auction.t.sol index a5ae09c..66434e4 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -28,7 +28,6 @@ contract AuctionTests is Base { assertEq(auction.step_duration(), stepDuration, "E5"); assertEq(auction.step_decay_rate(), stepDecayRate, "E6"); assertEq(auction.auction_length(), auctionLength, "E7"); - assertEq(auction.liquidation_auctions(), 0, "E8"); } function test_initialize_revertsIfAlreadyInitialized() public { @@ -36,11 +35,11 @@ contract AuctionTests is Base { auction.initialize( IAuction.InitializeParams({ papi: address(dutchDesk), - buyToken: address(borrowToken), - sellToken: address(collateralToken), - stepDuration: stepDuration, - stepDecayRate: stepDecayRate, - auctionLength: auctionLength + buy_token: address(borrowToken), + sell_token: address(collateralToken), + step_duration: stepDuration, + step_decay_rate: stepDecayRate, + auction_length: auctionLength }) ); } @@ -62,49 +61,21 @@ contract AuctionTests is Base { // Kick auction as dutchDesk vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, _maximumAmount, _startingPrice, _minimumPrice, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, _maximumAmount, _startingPrice, _minimumPrice, userLender, address(lender)); // Check auction state assertTrue(auction.is_active(_auctionId), "E0"); IAuction.AuctionInfo memory auctionInfo = auction.auctions(_auctionId); - assertEq(auctionInfo.kickTimestamp, block.timestamp, "E1"); - assertEq(auctionInfo.initialAmount, _kickAmount, "E2"); - assertEq(auctionInfo.currentAmount, _kickAmount, "E3"); - assertEq(auctionInfo.maximumAmount, _maximumAmount, "E4"); - assertEq(auctionInfo.amountReceived, 0, "E5"); - assertEq(auctionInfo.startingPrice, _startingPrice, "E6"); - assertEq(auctionInfo.minimumPrice, _minimumPrice, "E7"); + assertEq(auctionInfo.kick_timestamp, block.timestamp, "E1"); + assertEq(auctionInfo.initial_amount, _kickAmount, "E2"); + assertEq(auctionInfo.current_amount, _kickAmount, "E3"); + assertEq(auctionInfo.maximum_amount, _maximumAmount, "E4"); + assertEq(auctionInfo.amount_received, 0, "E5"); + assertEq(auctionInfo.starting_price, _startingPrice, "E6"); + assertEq(auctionInfo.minimum_price, _minimumPrice, "E7"); assertEq(auctionInfo.receiver, userLender, "E8"); - assertEq(auctionInfo.surplusReceiver, address(lender), "E9"); - assertFalse(auctionInfo.isLiquidation, "E10"); - assertFalse(auction.is_ongoing_liquidation_auction(), "E11"); - assertEq(auction.liquidation_auctions(), 0, "E12"); - assertEq(collateralToken.balanceOf(address(auction)), _kickAmount, "E13"); - } - - function test_kick_liquidation( - uint256 _auctionId, - uint256 _kickAmount, - uint256 _startingPrice, - uint256 _minimumPrice - ) public { - _kickAmount = bound(_kickAmount, minFuzzAmount, maxFuzzAmount); - _startingPrice = bound(_startingPrice, _kickAmount * 1e18, _kickAmount * 100e18); - _minimumPrice = bound(_minimumPrice, 1, _startingPrice / _kickAmount); - - // Airdrop collateral to dutchDesk - airdrop(address(collateralToken), address(dutchDesk), _kickAmount); - - // Kick liquidation auction (maximum_amount=0 for liquidations since all goes to receiver anyway) - vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _startingPrice, _minimumPrice, address(lender), address(lender), true); - - // Check liquidation state - assertTrue(auction.is_active(_auctionId), "E0"); - assertTrue(auction.auctions(_auctionId).isLiquidation, "E1"); - assertTrue(auction.is_ongoing_liquidation_auction(), "E2"); - assertEq(auction.liquidation_auctions(), 1, "E3"); - assertEq(auction.auctions(_auctionId).receiver, address(lender), "E4"); + assertEq(auctionInfo.surplus_receiver, address(lender), "E9"); + assertEq(collateralToken.balanceOf(address(auction)), _kickAmount, "E10"); } function test_kick_notPapi( @@ -120,7 +91,7 @@ contract AuctionTests is Base { // Should revert when called by non-PAPI vm.prank(userBorrower); vm.expectRevert("!papi"); - auction.kick(_auctionId, _kickAmount, 0, _startingPrice, _minimumPrice, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _startingPrice, _minimumPrice, userLender, address(lender)); } function test_kick_zeroAmount( @@ -128,7 +99,7 @@ contract AuctionTests is Base { ) public { vm.prank(address(dutchDesk)); vm.expectRevert("!kick_amount"); - auction.kick(_auctionId, 0, 0, 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, 0, 0, 1e18, 1e17, userLender, address(lender)); } function test_kick_zeroStartingPrice( @@ -139,7 +110,7 @@ contract AuctionTests is Base { vm.prank(address(dutchDesk)); vm.expectRevert("!starting_price"); - auction.kick(_auctionId, _kickAmount, 0, 0, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, 0, 1e17, userLender, address(lender)); } function test_kick_zeroMinimumPrice( @@ -152,7 +123,7 @@ contract AuctionTests is Base { vm.prank(address(dutchDesk)); vm.expectRevert("!minimum_price"); - auction.kick(_auctionId, _kickAmount, 0, _startingPrice, 0, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _startingPrice, 0, userLender, address(lender)); } function test_kick_zeroReceiver( @@ -167,7 +138,7 @@ contract AuctionTests is Base { vm.prank(address(dutchDesk)); vm.expectRevert("!receiver"); - auction.kick(_auctionId, _kickAmount, 0, _startingPrice, _minimumPrice, address(0), address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _startingPrice, _minimumPrice, address(0), address(lender)); } function test_kick_zeroSurplusReceiver( @@ -182,7 +153,7 @@ contract AuctionTests is Base { vm.prank(address(dutchDesk)); vm.expectRevert("!surplus_receiver"); - auction.kick(_auctionId, _kickAmount, 0, _startingPrice, _minimumPrice, userLender, address(0), false); + auction.kick(_auctionId, _kickAmount, 0, _startingPrice, _minimumPrice, userLender, address(0)); } function test_kick_auctionAlreadyActive( @@ -196,12 +167,12 @@ contract AuctionTests is Base { // First kick succeeds vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender)); // Second kick on same ID should revert vm.prank(address(dutchDesk)); vm.expectRevert("active"); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender)); } function test_reKick( @@ -213,7 +184,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender)); // Skip past auction duration so it becomes inactive skip(auction.auction_length() + 1); @@ -229,10 +200,10 @@ contract AuctionTests is Base { // Verify auction is active again with updated prices assertTrue(auction.is_active(_auctionId), "E2"); IAuction.AuctionInfo memory auctionInfo = auction.auctions(_auctionId); - assertEq(auctionInfo.currentAmount, _kickAmount, "E3"); - assertEq(auctionInfo.kickTimestamp, block.timestamp, "E4"); - assertEq(auctionInfo.startingPrice, _kickAmount * 1e18, "E5"); - assertEq(auctionInfo.minimumPrice, 1e17, "E6"); + assertEq(auctionInfo.current_amount, _kickAmount, "E3"); + assertEq(auctionInfo.kick_timestamp, block.timestamp, "E4"); + assertEq(auctionInfo.starting_price, _kickAmount * 1e18, "E5"); + assertEq(auctionInfo.minimum_price, 1e17, "E6"); } function test_reKick_notPapi( @@ -252,7 +223,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender)); // Try to re-kick while still active - should revert vm.prank(address(dutchDesk)); @@ -278,7 +249,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender)); // Skip past auction duration so it becomes inactive skip(auction.auction_length() + 1); @@ -298,7 +269,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender)); // Skip past auction duration so it becomes inactive skip(auction.auction_length() + 1); @@ -318,7 +289,7 @@ contract AuctionTests is Base { // Airdrop and kick with high maximum_amount (so no surplus) airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, type(uint256).max, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, type(uint256).max, _kickAmount * 1e18, 1e17, userLender, address(lender)); // Skip some time to let price decay skip(auction.step_duration() * 10); @@ -336,13 +307,13 @@ contract AuctionTests is Base { // Verify auction is complete assertFalse(auction.is_active(_auctionId), "E0"); IAuction.AuctionInfo memory auctionInfo = auction.auctions(_auctionId); - assertEq(auctionInfo.currentAmount, 0, "E1"); - assertEq(auctionInfo.kickTimestamp, 0, "E2"); + assertEq(auctionInfo.current_amount, 0, "E1"); + assertEq(auctionInfo.kick_timestamp, 0, "E2"); // Verify token transfers - all goes to receiver since neededAmount <= maximum_amount assertEq(collateralToken.balanceOf(liquidator), _kickAmount, "E3"); assertEq(borrowToken.balanceOf(userLender), _neededAmount, "E4"); - assertEq(auctionInfo.amountReceived, _neededAmount, "E5"); + assertEq(auctionInfo.amount_received, _neededAmount, "E5"); } function test_take_surplusToLender( @@ -361,7 +332,7 @@ contract AuctionTests is Base { // Airdrop and kick with low maximum_amount airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, _maximumAmount, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, _maximumAmount, _kickAmount * 1e18, 1e17, userLender, address(lender)); // Get actual needed amount uint256 _neededAmount = auction.get_needed_amount(_auctionId, type(uint256).max, block.timestamp); @@ -378,7 +349,7 @@ contract AuctionTests is Base { uint256 _surplus = _neededAmount > _maximumAmount ? _neededAmount - _maximumAmount : 0; assertEq(borrowToken.balanceOf(address(lender)) - _surplusReceiverBalanceBefore, _surplus, "E0"); assertEq(borrowToken.balanceOf(userLender), _maximumAmount, "E1"); - assertEq(auction.auctions(_auctionId).amountReceived, _maximumAmount, "E2"); + assertEq(auction.auctions(_auctionId).amount_received, _maximumAmount, "E2"); } function test_take_liquidationAllToReceiver( @@ -390,7 +361,7 @@ contract AuctionTests is Base { // Airdrop and kick as liquidation (receiver=lender for liquidations) airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, address(lender), address(lender), true); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, address(lender), address(lender)); // Skip some time to let price decay skip(auction.step_duration() * 10); @@ -419,7 +390,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, type(uint256).max, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, type(uint256).max, _kickAmount * 1e18, 1e17, userLender, address(lender)); // Skip some time skip(auction.step_duration() * 10); @@ -437,9 +408,9 @@ contract AuctionTests is Base { // Verify auction is still active with remaining amount assertTrue(auction.is_active(_auctionId), "E0"); IAuction.AuctionInfo memory auctionInfo = auction.auctions(_auctionId); - assertEq(auctionInfo.currentAmount, _kickAmount - _takeAmount, "E1"); + assertEq(auctionInfo.current_amount, _kickAmount - _takeAmount, "E1"); assertEq(collateralToken.balanceOf(liquidator), _takeAmount, "E2"); - assertEq(auctionInfo.amountReceived, _neededAmount, "E3"); + assertEq(auctionInfo.amount_received, _neededAmount, "E3"); } function test_take_multipleTakes_receiverCoveredFirst( @@ -452,7 +423,7 @@ contract AuctionTests is Base { // Kick auction airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(0, _kickAmount, _maximumAmount, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(0, _kickAmount, _maximumAmount, _kickAmount * 1e18, 1e17, userLender, address(lender)); uint256 _surplusReceiverBefore = borrowToken.balanceOf(address(lender)); @@ -465,11 +436,11 @@ contract AuctionTests is Base { vm.stopPrank(); // Second take: remaining - uint256 _needed2 = auction.get_needed_amount(0, auction.auctions(0).currentAmount, block.timestamp); + uint256 _needed2 = auction.get_needed_amount(0, auction.auctions(0).current_amount, block.timestamp); airdrop(address(borrowToken), liquidator, _needed2); vm.startPrank(liquidator); borrowToken.approve(address(auction), _needed2); - auction.take(0, auction.auctions(0).currentAmount, liquidator, ""); + auction.take(0, auction.auctions(0).current_amount, liquidator, ""); vm.stopPrank(); // Receiver capped at maximum_amount, surplus to surplus_receiver @@ -495,7 +466,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender)); // Try to take 0 amount vm.prank(liquidator); @@ -503,36 +474,6 @@ contract AuctionTests is Base { auction.take(_auctionId, 0, liquidator, ""); } - function test_take_liquidationDecreasesCounter( - uint256 _auctionId, - uint256 _kickAmount - ) public { - _kickAmount = bound(_kickAmount, minFuzzAmount, maxFuzzAmount); - - // Airdrop and kick as liquidation - airdrop(address(collateralToken), address(dutchDesk), _kickAmount); - vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, address(lender), address(lender), true); - - // Verify liquidation counter incremented - assertEq(auction.liquidation_auctions(), 1, "E0"); - assertTrue(auction.is_ongoing_liquidation_auction(), "E1"); - - // Skip time and take - skip(auction.step_duration() * 10); - uint256 _neededAmount = auction.get_needed_amount(_auctionId, type(uint256).max, block.timestamp); - airdrop(address(borrowToken), liquidator, _neededAmount); - - vm.startPrank(liquidator); - borrowToken.approve(address(auction), _neededAmount); - auction.take(_auctionId, type(uint256).max, liquidator, ""); - vm.stopPrank(); - - // Verify liquidation counter decremented - assertEq(auction.liquidation_auctions(), 0, "E2"); - assertFalse(auction.is_ongoing_liquidation_auction(), "E3"); - } - function test_priceDecay( uint256 _auctionId, uint256 _kickAmount @@ -542,7 +483,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender)); // Get initial price uint256 _initialPrice = auction.get_price(_auctionId, block.timestamp); @@ -574,7 +515,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender)); // Verify auction is active assertTrue(auction.is_active(_auctionId), "E0"); @@ -601,7 +542,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender)); // After kick, available amount should equal kick amount assertEq(auction.get_available_amount(_auctionId), _kickAmount, "E1"); @@ -623,7 +564,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender)); // While active, kickable amount should be 0 assertEq(auction.get_kickable_amount(_auctionId), 0, "E1"); @@ -642,7 +583,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, _kickAmount * 1e18, 1e17, userLender, address(lender)); // Get needed amount for full take uint256 _neededAmount = auction.get_needed_amount(_auctionId, _kickAmount, block.timestamp); @@ -670,7 +611,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender)); // Skip some time to let price decay skip(auction.step_duration() * 10); @@ -704,7 +645,7 @@ contract AuctionTests is Base { // Kick auction vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender)); // Skip some time skip(auction.step_duration() * 10); @@ -735,7 +676,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender)); // Skip some time skip(auction.step_duration() * 10); @@ -766,7 +707,7 @@ contract AuctionTests is Base { // Airdrop and kick airdrop(address(collateralToken), address(dutchDesk), _kickAmount); vm.prank(address(dutchDesk)); - auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender), false); + auction.kick(_auctionId, _kickAmount, 0, 1e18, 1e17, userLender, address(lender)); // Skip some time skip(auction.step_duration() * 10); @@ -844,7 +785,7 @@ contract ReentrantTaker { if (attackType == AttackType.Take) { auction.take(auctionId, takeAmount, address(this), ""); } else if (attackType == AttackType.Kick) { - auction.kick(auctionId, 1e18, 0, 1e18, 1e17, address(this), address(this), false); + auction.kick(auctionId, 1e18, 0, 1e18, 1e17, address(this), address(this)); } else if (attackType == AttackType.ReKick) { auction.re_kick(auctionId, 2e18, 2e17); } else if (attackType == AttackType.ViewFunctions) { diff --git a/test/AuctionPriceSimulation.t.sol b/test/AuctionPriceSimulation.t.sol index 95b57a7..f805df9 100644 --- a/test/AuctionPriceSimulation.t.sol +++ b/test/AuctionPriceSimulation.t.sol @@ -31,7 +31,7 @@ contract AuctionPriceSimulationTests is Base { uint256 marketPrice = 1000e18; // Starting price with buffer (e.g., 15% above market) - uint256 auctionStartingPrice = (marketPrice * redemptionStartingPriceBufferPercentage) / WAD; + uint256 auctionStartingPrice = (marketPrice * startingPriceBufferPercentage) / WAD; uint256 price = auctionStartingPrice; uint256 multiplier = 10000 - stepDecayRate; @@ -40,7 +40,7 @@ contract AuctionPriceSimulationTests is Base { console2.log(""); console2.log("=== Time to Reach Market Price ==="); console2.log("Market Price: %s", marketPrice / 1e18); - console2.log("Starting Price Buffer: %s%%", redemptionStartingPriceBufferPercentage / 1e16); + console2.log("Starting Price Buffer: %s%%", startingPriceBufferPercentage / 1e16); console2.log("Auction Starting Price: %s", auctionStartingPrice / 1e18); console2.log("Step Duration: %s seconds", stepDuration); console2.log("Step Decay Rate: %s basis points", stepDecayRate); diff --git a/test/Base.sol b/test/Base.sol index cdecbfe..6cbe8fa 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -40,11 +40,9 @@ abstract contract Base is Deploy, Test { uint256 public minimumCollateralRatio = 110; // 110% uint256 public upfrontInterestPeriod = 7 days; // 7 days uint256 public interestRateAdjCooldown = 7 days; // 7 days - uint256 public redemptionMinimumPriceBufferPercentage = 1e18 - 5e16; // 95% - uint256 public redemptionStartingPriceBufferPercentage = 1e18 + 1e16; // 101% - uint256 public redemptionReKickStartingPriceBufferPercentage = 1e18 + 20e16; // 120% - uint256 public liquidationMinimumPriceBufferPercentage = 1e18 - 10e16; // 90% - uint256 public liquidationStartingPriceBufferPercentage = 1e18 - 1e16; // 99% + uint256 public minimumPriceBufferPercentage = 1e18 - 5e16; // 95% + uint256 public startingPriceBufferPercentage = 1e18 + 1e16; // 101% + uint256 public reKickStartingPriceBufferPercentage = 1e18 + 10e16; // 110% uint256 public stepDuration = 20; // 20 seconds uint256 public stepDecayRate = 20; // 0.2% uint256 public auctionLength = 1 days; // 1 day @@ -79,23 +77,21 @@ abstract contract Base is Deploy, Test { // Deploy market (address _troveManager, address _sortedTroves, address _dutchDesk, address _auction, address _lender) = catFactory.deploy( ICatFactory.DeployParams({ - borrowToken: address(borrowToken), - collateralToken: address(collateralToken), - priceOracle: address(priceOracle), + borrow_token: address(borrowToken), + collateral_token: address(collateralToken), + price_oracle: address(priceOracle), management: management, - performanceFeeRecipient: performanceFeeRecipient, - minimumDebt: minimumDebt, - minimumCollateralRatio: minimumCollateralRatio, - upfrontInterestPeriod: upfrontInterestPeriod, - interestRateAdjCooldown: interestRateAdjCooldown, - redemptionMinimumPriceBufferPercentage: redemptionMinimumPriceBufferPercentage, - redemptionStartingPriceBufferPercentage: redemptionStartingPriceBufferPercentage, - redemptionReKickStartingPriceBufferPercentage: redemptionReKickStartingPriceBufferPercentage, - liquidationMinimumPriceBufferPercentage: liquidationMinimumPriceBufferPercentage, - liquidationStartingPriceBufferPercentage: liquidationStartingPriceBufferPercentage, - stepDuration: stepDuration, - stepDecayRate: stepDecayRate, - auctionLength: auctionLength, + performance_fee_recipient: performanceFeeRecipient, + minimum_debt: minimumDebt, + minimum_collateral_ratio: minimumCollateralRatio, + upfront_interest_period: upfrontInterestPeriod, + interest_rate_adj_cooldown: interestRateAdjCooldown, + minimum_price_buffer_percentage: minimumPriceBufferPercentage, + starting_price_buffer_percentage: startingPriceBufferPercentage, + re_kick_starting_price_buffer_percentage: reKickStartingPriceBufferPercentage, + step_duration: stepDuration, + step_decay_rate: stepDecayRate, + auction_length: auctionLength, salt: SALT }) ); diff --git a/test/Borrow.t.sol b/test/Borrow.t.sol index 2c9bed3..5d199aa 100644 --- a/test/Borrow.t.sol +++ b/test/Borrow.t.sol @@ -401,15 +401,14 @@ contract BorrowTests is Base { // Check starting price is set correctly (with buffer) assertApproxEqAbs( - auction.auctions(0).startingPrice, - _auctionAvailable * priceOracle.get_price(false) / WAD * dutchDesk.redemption_starting_price_buffer_percentage() - / COLLATERAL_TOKEN_PRECISION, + auction.auctions(0).starting_price, + _auctionAvailable * priceOracle.get_price(false) / WAD * dutchDesk.starting_price_buffer_percentage() / COLLATERAL_TOKEN_PRECISION, 3, "E53" ); // Check minimum price is set correctly (with buffer) - assertEq(auction.auctions(0).minimumPrice, priceOracle.get_price(false) * dutchDesk.redemption_minimum_price_buffer_percentage() / WAD, "E54"); + assertEq(auction.auctions(0).minimum_price, priceOracle.get_price(false) * dutchDesk.minimum_price_buffer_percentage() / WAD, "E54"); // Take the auction takeAuction(0); diff --git a/test/DutchDesk.t.sol b/test/DutchDesk.t.sol index c012d83..9db3139 100644 --- a/test/DutchDesk.t.sol +++ b/test/DutchDesk.t.sol @@ -26,70 +26,30 @@ contract DutchDeskTests is Base { assertEq(dutchDesk.auction(), address(auction), "E3"); assertEq(dutchDesk.collateral_token(), address(collateralToken), "E4"); assertEq(dutchDesk.collateral_token_precision(), COLLATERAL_TOKEN_PRECISION, "E5"); - assertEq(dutchDesk.redemption_minimum_price_buffer_percentage(), redemptionMinimumPriceBufferPercentage, "E6"); - assertEq(dutchDesk.redemption_starting_price_buffer_percentage(), redemptionStartingPriceBufferPercentage, "E7"); - assertEq(dutchDesk.redemption_re_kick_starting_price_buffer_percentage(), redemptionReKickStartingPriceBufferPercentage, "E8"); - assertEq(dutchDesk.liquidation_minimum_price_buffer_percentage(), liquidationMinimumPriceBufferPercentage, "E9"); - assertEq(dutchDesk.liquidation_starting_price_buffer_percentage(), liquidationStartingPriceBufferPercentage, "E10"); - assertEq(dutchDesk.nonce(), 0, "E11"); + assertEq(dutchDesk.minimum_price_buffer_percentage(), minimumPriceBufferPercentage, "E6"); + assertEq(dutchDesk.starting_price_buffer_percentage(), startingPriceBufferPercentage, "E7"); + assertEq(dutchDesk.re_kick_starting_price_buffer_percentage(), reKickStartingPriceBufferPercentage, "E8"); + assertEq(dutchDesk.nonce(), 0, "E9"); } function test_initialize_revertsIfAlreadyInitialized() public { vm.expectRevert("initialized"); dutchDesk.initialize( IDutchDesk.InitializeParams({ - troveManager: address(troveManager), + trove_manager: address(troveManager), lender: address(lender), - priceOracle: address(priceOracle), + price_oracle: address(priceOracle), auction: address(auction), - borrowToken: address(borrowToken), - collateralToken: address(collateralToken), - redemptionMinimumPriceBufferPercentage: redemptionMinimumPriceBufferPercentage, - redemptionStartingPriceBufferPercentage: redemptionStartingPriceBufferPercentage, - redemptionReKickStartingPriceBufferPercentage: redemptionReKickStartingPriceBufferPercentage, - liquidationMinimumPriceBufferPercentage: liquidationMinimumPriceBufferPercentage, - liquidationStartingPriceBufferPercentage: liquidationStartingPriceBufferPercentage + borrow_token: address(borrowToken), + collateral_token: address(collateralToken), + minimum_price_buffer_percentage: minimumPriceBufferPercentage, + starting_price_buffer_percentage: startingPriceBufferPercentage, + re_kick_starting_price_buffer_percentage: reKickStartingPriceBufferPercentage }) ); } - function test_kick_liquidation( - uint256 _amount - ) public { - _amount = bound(_amount, minFuzzAmount, maxFuzzAmount); - - // Collateral is transferred from troveManager via kick - airdrop(address(collateralToken), address(troveManager), _amount); - - uint256 _nonceBefore = dutchDesk.nonce(); - - // Liquidations: receiver=LENDER, maximum_amount=0, is_liquidation=true - vm.prank(address(troveManager)); - dutchDesk.kick(_amount, 0, address(lender), true); - - uint256 _auctionId = _nonceBefore; - - assertTrue(auction.is_active(_auctionId), "E0"); - assertEq(auction.get_available_amount(_auctionId), _amount, "E1"); - - IAuction.AuctionInfo memory auctionInfo = auction.auctions(_auctionId); - uint256 _expectedStartingPrice = - _amount * priceOracle.get_price(false) / WAD * dutchDesk.liquidation_starting_price_buffer_percentage() / COLLATERAL_TOKEN_PRECISION; - assertApproxEqAbs(auctionInfo.startingPrice, _expectedStartingPrice, 3, "E2"); - - uint256 _expectedMinimumPrice = priceOracle.get_price(false) * dutchDesk.liquidation_minimum_price_buffer_percentage() / WAD; - assertEq(auctionInfo.minimumPrice, _expectedMinimumPrice, "E3"); - - assertEq(auctionInfo.receiver, address(lender), "E4"); - assertEq(auctionInfo.surplusReceiver, address(lender), "E5"); - assertTrue(auctionInfo.isLiquidation, "E6"); - assertEq(dutchDesk.nonce(), _nonceBefore + 1, "E7"); - assertTrue(auction.is_ongoing_liquidation_auction(), "E8"); - assertEq(auction.liquidation_auctions(), 1, "E9"); - assertEq(auctionInfo.maximumAmount, 0, "E10"); - } - - function test_kick_redemption( + function test_kick( uint256 _amount, uint256 _maximumAmount, address _receiver @@ -104,7 +64,7 @@ contract DutchDeskTests is Base { uint256 _nonceBefore = dutchDesk.nonce(); vm.prank(address(troveManager)); - dutchDesk.kick(_amount, _maximumAmount, _receiver, false); + dutchDesk.kick(_amount, _maximumAmount, _receiver); uint256 _auctionId = _nonceBefore; @@ -113,55 +73,51 @@ contract DutchDeskTests is Base { IAuction.AuctionInfo memory auctionInfo = auction.auctions(_auctionId); uint256 _expectedStartingPrice = - _amount * priceOracle.get_price(false) * dutchDesk.redemption_starting_price_buffer_percentage() / WAD / COLLATERAL_TOKEN_PRECISION; - assertEq(auctionInfo.startingPrice, _expectedStartingPrice, "E2"); + _amount * priceOracle.get_price(false) * dutchDesk.starting_price_buffer_percentage() / WAD / COLLATERAL_TOKEN_PRECISION; + assertEq(auctionInfo.starting_price, _expectedStartingPrice, "E2"); - uint256 _expectedMinimumPrice = priceOracle.get_price(false) * dutchDesk.redemption_minimum_price_buffer_percentage() / WAD; - assertEq(auctionInfo.minimumPrice, _expectedMinimumPrice, "E3"); + uint256 _expectedMinimumPrice = priceOracle.get_price(false) * dutchDesk.minimum_price_buffer_percentage() / WAD; + assertEq(auctionInfo.minimum_price, _expectedMinimumPrice, "E3"); assertEq(auctionInfo.receiver, _receiver, "E4"); - assertEq(auctionInfo.surplusReceiver, address(lender), "E5"); - assertFalse(auctionInfo.isLiquidation, "E6"); - assertEq(dutchDesk.nonce(), _nonceBefore + 1, "E7"); - assertFalse(auction.is_ongoing_liquidation_auction(), "E8"); - assertEq(auction.liquidation_auctions(), 0, "E9"); - assertEq(auctionInfo.maximumAmount, _maximumAmount, "E10"); + assertEq(auctionInfo.surplus_receiver, address(lender), "E5"); + assertEq(dutchDesk.nonce(), _nonceBefore + 1, "E6"); + assertEq(auctionInfo.maximum_amount, _maximumAmount, "E7"); } function test_kick_zeroAmount( - address _receiver, - bool _isLiquidation + address _receiver ) public { uint256 _nonceBefore = dutchDesk.nonce(); vm.prank(address(troveManager)); - dutchDesk.kick(0, 0, _receiver, _isLiquidation); + dutchDesk.kick(0, 0, _receiver); // Nothing should happen - nonce unchanged, no active auction assertEq(dutchDesk.nonce(), _nonceBefore, "E0"); assertFalse(auction.is_active(_nonceBefore), "E1"); } - function test_kick_multipleAuctions( + function test_multiple_kicks( uint256 _amount1, uint256 _amount2 ) public { _amount1 = bound(_amount1, minFuzzAmount, maxFuzzAmount / 2); _amount2 = bound(_amount2, minFuzzAmount, maxFuzzAmount / 2); - // First kick (liquidation) + // First kick (redemption) airdrop(address(collateralToken), address(troveManager), _amount1); vm.prank(address(troveManager)); - dutchDesk.kick(_amount1, 0, address(lender), true); + dutchDesk.kick(_amount1, type(uint256).max, address(userLender)); assertTrue(auction.is_active(0), "E0"); assertEq(auction.get_available_amount(0), _amount1, "E1"); assertEq(dutchDesk.nonce(), 1, "E2"); - // Second kick creates a new auction with nonce 1 (liquidation) + // Second kick creates a new auction with nonce 1 (redemption) airdrop(address(collateralToken), address(troveManager), _amount2); vm.prank(address(troveManager)); - dutchDesk.kick(_amount2, 0, address(lender), true); + dutchDesk.kick(_amount2, type(uint256).max, address(userLender)); assertTrue(auction.is_active(1), "E3"); assertEq(auction.get_available_amount(1), _amount2, "E4"); @@ -169,7 +125,6 @@ contract DutchDeskTests is Base { // First auction is still active assertTrue(auction.is_active(0), "E6"); - assertEq(auction.liquidation_auctions(), 2, "E7"); } function test_reKick( @@ -182,7 +137,7 @@ contract DutchDeskTests is Base { // Kick a redemption auction airdrop(address(collateralToken), address(troveManager), _amount); vm.prank(address(troveManager)); - dutchDesk.kick(_amount, type(uint256).max, _receiver, false); + dutchDesk.kick(_amount, type(uint256).max, _receiver); uint256 _auctionId = 0; assertTrue(auction.is_active(_auctionId), "E0"); @@ -199,14 +154,14 @@ contract DutchDeskTests is Base { assertTrue(auction.is_active(_auctionId), "E3"); assertEq(auction.get_available_amount(_auctionId), _amount, "E4"); - // Starting price should use EMERGENCY buffer + // Starting price should use re-kick buffer IAuction.AuctionInfo memory auctionInfo = auction.auctions(_auctionId); - uint256 _expectedStartingPrice = _amount * priceOracle.get_price(false) * dutchDesk.redemption_re_kick_starting_price_buffer_percentage() - / WAD / COLLATERAL_TOKEN_PRECISION; - assertEq(auctionInfo.startingPrice, _expectedStartingPrice, "E5"); + uint256 _expectedStartingPrice = + _amount * priceOracle.get_price(false) * dutchDesk.re_kick_starting_price_buffer_percentage() / WAD / COLLATERAL_TOKEN_PRECISION; + assertEq(auctionInfo.starting_price, _expectedStartingPrice, "E5"); - uint256 _expectedMinimumPrice = priceOracle.get_price(false) * dutchDesk.redemption_minimum_price_buffer_percentage() / WAD; - assertEq(auctionInfo.minimumPrice, _expectedMinimumPrice, "E6"); + uint256 _expectedMinimumPrice = priceOracle.get_price(false) * dutchDesk.minimum_price_buffer_percentage() / WAD; + assertEq(auctionInfo.minimum_price, _expectedMinimumPrice, "E6"); } function test_reKick_auctionStillActive( @@ -219,7 +174,7 @@ contract DutchDeskTests is Base { // Kick a redemption auction airdrop(address(collateralToken), address(troveManager), _amount); vm.prank(address(troveManager)); - dutchDesk.kick(_amount, type(uint256).max, _receiver, false); + dutchDesk.kick(_amount, type(uint256).max, _receiver); assertTrue(auction.is_active(0), "E0"); diff --git a/test/Gas.t.sol b/test/Gas.t.sol index 1793709..7e5ae4b 100644 --- a/test/Gas.t.sol +++ b/test/Gas.t.sol @@ -89,42 +89,44 @@ contract GasTests is Base { assertLt(gasUsed, MAX_GAS, "Exceeded 7M gas limit"); } - function test_gas_liquidateTroves() public { - uint256 _minDebt = troveManager.min_debt(); - uint256 _rate = DEFAULT_ANNUAL_INTEREST_RATE; - uint256 _numTroves = MAX_LIQUIDATIONS; + // @todo + // function test_gas_liquidateTroves() public { + // uint256 _minDebt = troveManager.min_debt(); + // uint256 _rate = DEFAULT_ANNUAL_INTEREST_RATE; + // uint256 _numTroves = MAX_LIQUIDATIONS; - uint256 _lenderDeposit = _minDebt * _numTroves; + // uint256 _lenderDeposit = _minDebt * _numTroves; - // Fund lender - mintAndDepositIntoLender(userLender, _lenderDeposit); + // // Fund lender + // mintAndDepositIntoLender(userLender, _lenderDeposit); - // Create troves and store their IDs - uint256[MAX_LIQUIDATIONS] memory _troveIds; - for (uint256 i = 0; i < _numTroves; i++) { - uint256 _collateralNeeded = - (_minDebt * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + // // Create troves and store their IDs + // uint256[MAX_LIQUIDATIONS] memory _troveIds; + // for (uint256 i = 0; i < _numTroves; i++) { + // uint256 _collateralNeeded = + // (_minDebt * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - address _user = address(uint160(i + 1000)); - _troveIds[i] = mintAndOpenTrove(_user, _collateralNeeded, _minDebt, _rate); - } + // address _user = address(uint160(i + 1000)); + // _troveIds[i] = mintAndOpenTrove(_user, _collateralNeeded, _minDebt, _rate); + // } - // Drop price to make all troves liquidatable - uint256 _priceDropToBelowMCR = priceOracle.get_price() * 80 / 100; // 20% drop - uint256 _priceDropToBelowMCR18 = priceOracle.get_price(false) * 80 / 100; // 20% drop - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); + // // Drop price to make all troves liquidatable + // uint256 _priceDropToBelowMCR = priceOracle.get_price() * 80 / 100; // 20% drop + // uint256 _priceDropToBelowMCR18 = priceOracle.get_price(false) * 80 / 100; // 20% drop + // vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); + // vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - // Liquidate all troves - uint256 _gasBefore = gasleft(); - troveManager.liquidate_troves(_troveIds); - uint256 _gasUsed = _gasBefore - gasleft(); + // // Liquidate all troves + // uint256 _gasBefore = gasleft(); + // troveManager.liquidate_troves(_troveIds); + // uint256 _gasUsed = _gasBefore - gasleft(); - emit log_named_uint("Troves liquidated", _numTroves); - emit log_named_uint("Gas used", _gasUsed); - emit log_named_uint("Cost in ETH (wei)", _gasUsed * GAS_PRICE); + // emit log_named_uint("Troves liquidated", _numTroves); + // emit log_named_uint("Gas used", _gasUsed); + // emit log_named_uint("Cost in ETH (wei)", _gasUsed * GAS_PRICE); + + // assertLt(_gasUsed, MAX_GAS + 1_000_000, "Exceeded 8M gas limit"); + // } - assertLt(_gasUsed, MAX_GAS + 1_000_000, "Exceeded 8M gas limit"); - } } diff --git a/test/LaggingOracleValueExtractionPOC.t.sol b/test/LaggingOracleValueExtractionPOC.t.sol index a475798..ebecc97 100644 --- a/test/LaggingOracleValueExtractionPOC.t.sol +++ b/test/LaggingOracleValueExtractionPOC.t.sol @@ -33,7 +33,7 @@ contract LaggingOracleValueExtractionPOC is Base { // Max market premium must be less than starting price buffer, otherwise attacker can still profit // STARTING_PRICE_BUFFER_PERCENTAGE is 1e18 + buffer%, e.g. 1.15e18 for 15% - uint256 _startingBufferBps = (dutchDesk.redemption_starting_price_buffer_percentage() - WAD) / 1e14; + uint256 _startingBufferBps = (dutchDesk.starting_price_buffer_percentage() - WAD) / 1e14; _marketPremiumBps = bound(_marketPremiumBps, 10, _startingBufferBps - 1); // Lend liquidity @@ -61,7 +61,7 @@ contract LaggingOracleValueExtractionPOC is Base { assertEq(auction.auctions(0).receiver, userBorrower, "E0"); uint256 _auctionCollateral = auction.get_available_amount(0); - uint256 _maximumAmount = auction.auctions(0).maximumAmount; + uint256 _maximumAmount = auction.auctions(0).maximum_amount; // Track lender balance before auction take uint256 _lenderBalanceBefore = borrowToken.balanceOf(address(lender)); diff --git a/test/Lend.t.sol b/test/Lend.t.sol index 8ccef8d..c770cfa 100644 --- a/test/Lend.t.sol +++ b/test/Lend.t.sol @@ -1,9 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {IPriceOracleNotScaled} from "./interfaces/IPriceOracleNotScaled.sol"; -import {IPriceOracleScaled} from "./interfaces/IPriceOracleScaled.sol"; - import "./Base.sol"; contract LendTests is Base { @@ -28,7 +25,6 @@ contract LendTests is Base { } function test_setup() public { - assertEq(address(lender.AUCTION()), address(auction), "E0"); assertEq(address(lender.TROVE_MANAGER()), address(troveManager), "E1"); assertEq(lender.depositLimit(), type(uint256).max, "E2"); assertEq(lender.availableWithdrawLimit(userLender), type(uint256).max, "E3"); @@ -442,163 +438,4 @@ contract LendTests is Base { lender.setDepositLimit(_depositLimit); } - function test_availableWithdrawLimit_duringLiquidation( - uint256 _amount, - uint256 _idleLiquidity - ) public { - _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); - _idleLiquidity = bound(_idleLiquidity, minFuzzAmount, maxFuzzAmount); - - // Lend some from lender - mintAndDepositIntoLender(userLender, _amount); - - // Lend from another user so we have some idle liquidity after borrow - address _anotherUserLender = address(0xBEEF); - - // Lend some from lender - mintAndDepositIntoLender(_anotherUserLender, _idleLiquidity); - - // Calculate how much collateral is needed for the borrow amount - uint256 _collateralNeeded = - (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - - // Open a trove - uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Get trove info - ITroveManager.Trove memory _trove = troveManager.troves(_troveId); - - // No ongoing liquidation yet - assertEq(lender.availableWithdrawLimit(userLender), type(uint256).max, "E0"); - assertFalse(auction.is_ongoing_liquidation_auction(), "E1"); - - // Calculate price drop to put trove below MCR (1% below) - uint256 _priceDropToBelowMCR; - if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { - _priceDropToBelowMCR = - troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); - } else { - _priceDropToBelowMCR = - troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; - } - uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); - - // Mock the oracle price - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - - // Liquidate the trove - uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; - _troveIdsToLiquidate[0] = _troveId; - troveManager.liquidate_troves(_troveIdsToLiquidate); - - // Liquidation auction is now ongoing - assertTrue(auction.is_ongoing_liquidation_auction(), "E2"); - - // Available withdraw limit should be only idle assets during liquidation - assertEq(lender.availableWithdrawLimit(userLender), _idleLiquidity, "E3"); - - // Take the liquidation auction - takeAuction(0); - - // After auction is complete, no more ongoing liquidation - assertFalse(auction.is_ongoing_liquidation_auction(), "E4"); - - // Available withdraw limit should be max again - assertEq(lender.availableWithdrawLimit(userLender), type(uint256).max, "E5"); - } - - function test_shutdownCanWithdraw( - uint256 _amount - ) public { - _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); - - // Lend some from lender - mintAndDepositIntoLender(userLender, _amount); - - assertEq(lender.totalAssets(), _amount, "E0"); - - // Calculate how much collateral is needed for the borrow amount - uint256 _collateralNeeded = - (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - - // Open a trove - mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Skip some time - skip(1 days); - - // Shutdown the strategy - vm.prank(management); - lender.shutdownStrategy(); - - // Available withdraw limit should still be max when shutdown - assertEq(lender.availableWithdrawLimit(userLender), type(uint256).max, "E1"); - - // Make sure we can still withdraw the full amount - uint256 _balanceBefore = borrowToken.balanceOf(userLender); - - // Withdraw all funds - vm.prank(userLender); - lender.redeem(_amount, userLender, userLender); - - // Take the auction - takeAuction(0); - - // No report, no profit, loss bc `takeAuction` pricing is not perfect - assertApproxEqRel(borrowToken.balanceOf(userLender), _balanceBefore + _amount, 5e15, "E31"); // 0.5% - } - - function test_shutdownCanWithdraw_duringLiquidation( - uint256 _amount - ) public { - _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); - - // Lend some from lender - mintAndDepositIntoLender(userLender, _amount); - - // Calculate how much collateral is needed for the borrow amount - uint256 _collateralNeeded = - (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - - // Open a trove - uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Get trove info - ITroveManager.Trove memory _trove = troveManager.troves(_troveId); - - // Calculate price drop to put trove below MCR (1% below) - uint256 _priceDropToBelowMCR; - if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { - _priceDropToBelowMCR = - troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); - } else { - _priceDropToBelowMCR = - troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; - } - uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); - - // Mock the oracle price - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - - // Liquidate the trove - uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; - _troveIdsToLiquidate[0] = _troveId; - troveManager.liquidate_troves(_troveIdsToLiquidate); - - // Liquidation auction is now ongoing - assertTrue(auction.is_ongoing_liquidation_auction(), "E0"); - - // Available withdraw limit should be 0 during liquidation - assertEq(lender.availableWithdrawLimit(userLender), 0, "E1"); - - // Shutdown the strategy - vm.prank(management); - lender.shutdownStrategy(); - - // When shutdown, available withdraw limit should be max even during liquidation - assertEq(lender.availableWithdrawLimit(userLender), type(uint256).max, "E2"); - } - } diff --git a/test/Liquidate.t.sol b/test/Liquidate.t.sol index 665b697..a3a5bc0 100644 --- a/test/Liquidate.t.sol +++ b/test/Liquidate.t.sol @@ -1,666 +1,666 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import "./Base.sol"; -import {IPriceOracleNotScaled} from "./interfaces/IPriceOracleNotScaled.sol"; -import {IPriceOracleScaled} from "./interfaces/IPriceOracleScaled.sol"; - -contract LiquidateTests is Base { - - function setUp() public override { - Base.setUp(); - - // Set `profitMaxUnlockTime` to 0 - vm.prank(management); - lender.setProfitMaxUnlockTime(0); - - // Set fees to 0 - vm.prank(management); - lender.setPerformanceFee(0); - } - - // 1. lend - // 2. borrow all available liquidity - // 3. collateral price drops - // 4. liquidate trove - function test_liquidateTrove( - uint256 _amount - ) public { - _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); - - // Lend some from lender - mintAndDepositIntoLender(userLender, _amount); - - // Calculate how much collateral is needed for the borrow amount - uint256 _collateralNeeded = - (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - - // Calculate expected debt (borrow amount + upfront fee) - uint256 _expectedDebt = _amount + troveManager.get_upfront_fee(_amount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Open a trove - uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Check trove info - ITroveManager.Trove memory _trove = troveManager.troves(_troveId); - assertEq(_trove.debt, _expectedDebt, "E0"); - assertEq(_trove.collateral, _collateralNeeded, "E1"); - assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E2"); - assertEq(_trove.last_debt_update_time, block.timestamp, "E3"); - assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E4"); - assertEq(_trove.owner, userBorrower, "E5"); - assertEq(_trove.pending_owner, address(0), "E6"); - assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E7"); - assertApproxEqRel( - (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, - DEFAULT_TARGET_COLLATERAL_RATIO, - 1e15, - "E8" - ); // 0.1% - - // Check sorted troves - assertFalse(sortedTroves.empty(), "E9"); - assertEq(sortedTroves.size(), 1, "E10"); - assertEq(sortedTroves.first(), _troveId, "E11"); - assertEq(sortedTroves.last(), _troveId, "E12"); - assertTrue(sortedTroves.contains(_troveId), "E13"); - - // Check balances - assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded, "E14"); - assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E15"); - assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E16"); - assertEq(borrowToken.balanceOf(address(lender)), 0, "E17"); - assertEq(borrowToken.balanceOf(userBorrower), _amount, "E18"); - - // Check global info - assertEq(troveManager.total_debt(), _expectedDebt, "E19"); - assertEq(troveManager.total_weighted_debt(), _expectedDebt * DEFAULT_ANNUAL_INTEREST_RATE, "E20"); - assertEq(troveManager.collateral_balance(), _collateralNeeded, "E21"); - assertEq(troveManager.zombie_trove_id(), 0, "E22"); - - // Check dutch desk is empty - assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E23"); - assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E24"); - - // CR = (collateral * price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / debt - // So price_at_MCR = MCR * debt * ORACLE_PRICE_SCALE / (collateral * BORROW_TOKEN_PRECISION) - // We want to be 1% below MCR - uint256 _priceDropToBelowMCR; - if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { - // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow - _priceDropToBelowMCR = - troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); - } else { - // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow - _priceDropToBelowMCR = - troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; - } - uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); - - // Drop collateral price to put trove below MCR - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - - // Make sure price actually dropped - assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E25"); - - // Calculate Trove's collateral ratio after price drop - uint256 _troveCollateralRatioAfter = (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; - - // Make sure Trove is below MCR - assertLt(_troveCollateralRatioAfter, troveManager.minimum_collateral_ratio(), "E26"); - - // Finally, liquidate the trove - { - vm.startPrank(liquidator); - uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; - _troveIdsToLiquidate[0] = _troveId; - troveManager.liquidate_troves(_troveIdsToLiquidate); - vm.stopPrank(); - } - - // Check liquidator received no fee (all collateral goes to auction) - assertEq(collateralToken.balanceOf(liquidator), 0, "E26a"); - - // Check auction starting price and minimum price - { - uint256 _expectedStartingPrice = _collateralNeeded * _priceDropToBelowMCR18 * dutchDesk.liquidation_starting_price_buffer_percentage() - / 1e18 / COLLATERAL_TOKEN_PRECISION; - assertEq(auction.auctions(0).startingPrice, _expectedStartingPrice, "E27"); - uint256 _expectedMinimumPrice = _priceDropToBelowMCR18 * dutchDesk.liquidation_minimum_price_buffer_percentage() / WAD; - assertEq(auction.auctions(0).minimumPrice, _expectedMinimumPrice, "E28"); - } - - // Take the auction - takeAuction(0); - - // Make sure lender got all the borrow tokens back + liquidation fee - assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E29"); - - // Make sure liquidator got the collateral - assertEq(collateralToken.balanceOf(liquidator), _collateralNeeded, "E30"); - - // Check everything again - - // Check trove info - _trove = troveManager.troves(_troveId); - assertEq(_trove.debt, 0, "E31"); - assertEq(_trove.collateral, 0, "E32"); - assertEq(_trove.annual_interest_rate, 0, "E33"); - assertEq(_trove.last_debt_update_time, 0, "E34"); - assertEq(_trove.last_interest_rate_adj_time, 0, "E35"); - assertEq(_trove.owner, address(0), "E36"); - assertEq(_trove.pending_owner, address(0), "E37"); - assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E38"); - - // Check sorted troves - assertTrue(sortedTroves.empty(), "E39"); - assertEq(sortedTroves.size(), 0, "E40"); - assertEq(sortedTroves.first(), 0, "E41"); - assertEq(sortedTroves.last(), 0, "E42"); - assertFalse(sortedTroves.contains(_troveId), "E43"); - - // Check balances - assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E44"); - assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E45"); - assertEq(collateralToken.balanceOf(address(userBorrower)), 0, "E46"); - assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E47"); - assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E48"); - assertEq(borrowToken.balanceOf(userBorrower), _amount, "E49"); - - // Check global info - assertEq(troveManager.total_debt(), 0, "E50"); - assertEq(troveManager.total_weighted_debt(), 0, "E51"); - assertEq(troveManager.collateral_balance(), 0, "E52"); - assertEq(troveManager.zombie_trove_id(), 0, "E53"); - - // Check dutch desk is empty - assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E54"); - assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E55"); - } - - // 1. lend - // 2. borrow half of available liquidity from 1st borrower - // 3. borrow half of available liquidity from 2nd borrower - // 4. collateral price drops - // 5. liquidate both troves - function test_liquidateTroves( - uint256 _amount - ) public { - _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); - - // Lend some from lender - mintAndDepositIntoLender(userLender, _amount); - - uint256 _halfAmount = _amount / 2; - - // Calculate how much collateral is needed for the borrow amount - uint256 _collateralNeeded = - (_halfAmount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - - // Calculate expected debt (borrow amount + upfront fee) - uint256 _expectedDebt = _halfAmount + troveManager.get_upfront_fee(_halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Open a trove for the first borrower - uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Check trove info - ITroveManager.Trove memory _trove = troveManager.troves(_troveId); - assertEq(_trove.debt, _expectedDebt, "E0"); - assertEq(_trove.collateral, _collateralNeeded, "E1"); - assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E2"); - assertEq(_trove.last_debt_update_time, block.timestamp, "E3"); - assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E4"); - assertEq(_trove.owner, userBorrower, "E5"); - assertEq(_trove.pending_owner, address(0), "E6"); - assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E7"); - assertApproxEqRel( - (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, - DEFAULT_TARGET_COLLATERAL_RATIO, - 1e15, - "E8" - ); // 0.1% - - // Check sorted troves - assertFalse(sortedTroves.empty(), "E9"); - assertEq(sortedTroves.size(), 1, "E10"); - assertEq(sortedTroves.first(), _troveId, "E11"); - assertEq(sortedTroves.last(), _troveId, "E12"); - assertTrue(sortedTroves.contains(_troveId), "E13"); - - // Check balances - assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded, "E14"); - assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E15"); - assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E16"); - assertApproxEqAbs(borrowToken.balanceOf(address(lender)), _halfAmount, 1, "E17"); - assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E18"); - - // Check global info - assertEq(troveManager.total_debt(), _expectedDebt, "E19"); - assertEq(troveManager.total_weighted_debt(), _expectedDebt * DEFAULT_ANNUAL_INTEREST_RATE, "E20"); - assertEq(troveManager.collateral_balance(), _collateralNeeded, "E21"); - assertEq(troveManager.zombie_trove_id(), 0, "E22"); - - // Check dutch desk is empty - assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E23"); - assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E24"); - - // Open a trove for the second borrower - uint256 _anotherTroveId = mintAndOpenTrove(anotherUserBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Check trove info - _trove = troveManager.troves(_anotherTroveId); - assertEq(_trove.debt, _expectedDebt, "E25"); - assertEq(_trove.collateral, _collateralNeeded, "E26"); - assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E27"); - assertEq(_trove.last_debt_update_time, block.timestamp, "E28"); - assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E29"); - assertEq(_trove.owner, anotherUserBorrower, "E30"); - assertEq(_trove.pending_owner, address(0), "E31"); - assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E32"); - assertApproxEqRel( - (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, - DEFAULT_TARGET_COLLATERAL_RATIO, - 1e15, - "E33" - ); // 0.1% - - // Check sorted troves - assertFalse(sortedTroves.empty(), "E34"); - assertEq(sortedTroves.size(), 2, "E35"); - assertEq(sortedTroves.first(), _troveId, "E36"); - assertEq(sortedTroves.last(), _anotherTroveId, "E37"); - assertTrue(sortedTroves.contains(_troveId), "E38"); - assertTrue(sortedTroves.contains(_anotherTroveId), "E39"); - - // Check balances - assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded * 2, "E40"); - assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E41"); - assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E42"); - assertApproxEqAbs(borrowToken.balanceOf(address(lender)), 0, 1, "E43"); - assertEq(borrowToken.balanceOf(anotherUserBorrower), _halfAmount, "E44"); - - // Check global info - assertEq(troveManager.total_debt(), _expectedDebt * 2, "E45"); - assertEq(troveManager.total_weighted_debt(), _expectedDebt * 2 * DEFAULT_ANNUAL_INTEREST_RATE, "E46"); - assertEq(troveManager.collateral_balance(), _collateralNeeded * 2, "E47"); - assertEq(troveManager.zombie_trove_id(), 0, "E48"); - - // Check dutch desk is empty - assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E49"); - assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E50"); - - // CR = (collateral * price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / debt - // So price_at_MCR = MCR * debt * ORACLE_PRICE_SCALE / (collateral * BORROW_TOKEN_PRECISION) - // We want to be 1% below MCR - uint256 _priceDropToBelowMCR; - if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { - // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow - _priceDropToBelowMCR = - troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); - } else { - // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow - _priceDropToBelowMCR = - troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; - } - uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); - - // Drop collateral price to put trove below MCR - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - - // Make sure price actually dropped - assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E51"); - - // Calculate Trove's collateral ratio after price drop - uint256 _troveCollateralRatioAfter = (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; - - // Make sure Trove is below MCR - assertLt(_troveCollateralRatioAfter, troveManager.minimum_collateral_ratio(), "E52"); - - // Finally, liquidate the troves - { - vm.startPrank(liquidator); - uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; - _troveIdsToLiquidate[0] = _troveId; - _troveIdsToLiquidate[1] = _anotherTroveId; - troveManager.liquidate_troves(_troveIdsToLiquidate); - vm.stopPrank(); - } - - // Check liquidator received no fee (all collateral goes to auction) - assertEq(collateralToken.balanceOf(liquidator), 0, "E53"); - - // Check auction starting price and minimum price - // Starting price = available * price * STARTING_PRICE_BUFFER_PERCENTAGE / 1e18 / COLLATERAL_TOKEN_PRECISION - // Note: both troves liquidated in same tx, so all collateral goes to auction - { - uint256 _collateralToSell = _collateralNeeded * 2; - uint256 _expectedStartingPrice = _collateralToSell * _priceDropToBelowMCR18 * dutchDesk.liquidation_starting_price_buffer_percentage() - / 1e18 / COLLATERAL_TOKEN_PRECISION; - assertEq(auction.auctions(0).startingPrice, _expectedStartingPrice, "E54"); - uint256 _expectedMinimumPrice = _priceDropToBelowMCR18 * dutchDesk.liquidation_minimum_price_buffer_percentage() / WAD; - assertEq(auction.auctions(0).minimumPrice, _expectedMinimumPrice, "E55"); - } - - // Take the auction - takeAuction(0); - - // Make sure lender got all the borrow tokens back + liquidation fee - assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt * 2, "E56"); - - // Make sure liquidator got the collateral - assertEq(collateralToken.balanceOf(liquidator), _collateralNeeded * 2, "E57"); - - // Check everything again - - // Check trove info - _trove = troveManager.troves(_troveId); - assertEq(_trove.debt, 0, "E58"); - assertEq(_trove.collateral, 0, "E59"); - assertEq(_trove.annual_interest_rate, 0, "E60"); - assertEq(_trove.last_debt_update_time, 0, "E61"); - assertEq(_trove.last_interest_rate_adj_time, 0, "E62"); - assertEq(_trove.owner, address(0), "E63"); - assertEq(_trove.pending_owner, address(0), "E64"); - assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E65"); - - // Check sorted troves - assertTrue(sortedTroves.empty(), "E66"); - assertEq(sortedTroves.size(), 0, "E67"); - assertEq(sortedTroves.first(), 0, "E68"); - assertEq(sortedTroves.last(), 0, "E69"); - assertFalse(sortedTroves.contains(_troveId), "E70"); - - // Check balances - assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E71"); - assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E72"); - assertEq(collateralToken.balanceOf(address(userBorrower)), 0, "E73"); - assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E74"); - assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E75"); - assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E76"); - - // Check global info - assertEq(troveManager.total_debt(), 0, "E77"); - assertEq(troveManager.total_weighted_debt(), 0, "E78"); - assertEq(troveManager.collateral_balance(), 0, "E79"); - assertEq(troveManager.zombie_trove_id(), 0, "E80"); - - // Check dutch desk is empty - assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E81"); - assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E82"); - - // Check everything again for the second trove - - // Check trove info - _trove = troveManager.troves(_anotherTroveId); - assertEq(_trove.debt, 0, "E83"); - assertEq(_trove.collateral, 0, "E84"); - assertEq(_trove.annual_interest_rate, 0, "E85"); - assertEq(_trove.last_debt_update_time, 0, "E86"); - assertEq(_trove.last_interest_rate_adj_time, 0, "E87"); - assertEq(_trove.owner, address(0), "E88"); - assertEq(_trove.pending_owner, address(0), "E89"); - assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E90"); - - // Check sorted troves - assertTrue(sortedTroves.empty(), "E91"); - assertEq(sortedTroves.size(), 0, "E92"); - assertEq(sortedTroves.first(), 0, "E93"); - assertEq(sortedTroves.last(), 0, "E94"); - assertFalse(sortedTroves.contains(_anotherTroveId), "E95"); - - // Check balances - assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E96"); - assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E97"); - assertEq(collateralToken.balanceOf(address(userBorrower)), 0, "E98"); - assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E99"); - assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E100"); - assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E101"); - - // Check global info - assertEq(troveManager.total_debt(), 0, "E102"); - assertEq(troveManager.total_weighted_debt(), 0, "E103"); - assertEq(troveManager.collateral_balance(), 0, "E104"); - assertEq(troveManager.zombie_trove_id(), 0, "E105"); - - // Check dutch desk is empty - assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E106"); - assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E107"); - } - - // 1. lend - // 2. open 2 troves - // 3. collateral price drops - // 4. liquidate first trove - // 5. liquidate second trove (before taking the first auction) - // 6. take the auction (should have collateral from both troves - DutchDesk sweeps + settles + re-kicks with all collateral) - function test_liquidateTroves_sequentialLiquidations( - uint256 _amount - ) public { - _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); - - // Lend some from lender - mintAndDepositIntoLender(userLender, _amount); - - uint256 _halfAmount = _amount / 2; - - // Calculate how much collateral is needed for the borrow amount - uint256 _collateralNeeded = - (_halfAmount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - - // Calculate expected debt (borrow amount + upfront fee) - uint256 _expectedDebt = _halfAmount + troveManager.get_upfront_fee(_halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Open first trove - uint256 _troveId1 = mintAndOpenTrove(userBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Open second trove - uint256 _troveId2 = mintAndOpenTrove(anotherUserBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Check trove info for first trove - ITroveManager.Trove memory _trove1 = troveManager.troves(_troveId1); - assertEq(_trove1.debt, _expectedDebt, "E0"); - assertEq(_trove1.collateral, _collateralNeeded, "E1"); - - // Check trove info for second trove - ITroveManager.Trove memory _trove2 = troveManager.troves(_troveId2); - assertEq(_trove2.debt, _expectedDebt, "E2"); - assertEq(_trove2.collateral, _collateralNeeded, "E3"); - - // Check dutch desk is empty - assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E4"); - assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E5"); - - // CR = collateral * price / debt, so price_at_MCR = MCR * debt / collateral - // We want to be 1% below MCR - uint256 _priceDropToBelowMCR; - if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { - // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow - _priceDropToBelowMCR = troveManager.minimum_collateral_ratio() * _trove1.debt * ORACLE_PRICE_SCALE * 99 - / (100 * _trove1.collateral * BORROW_TOKEN_PRECISION); - } else { - // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow - _priceDropToBelowMCR = - troveManager.minimum_collateral_ratio() * _trove1.debt * 99 / 100 / _trove1.collateral * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION; - } - uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); - - // Drop collateral price to put both troves below MCR - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - - // Make sure price actually dropped - assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E6"); - - // Make sure both troves are below MCR - assertLt( - (_trove1.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove1.debt, - troveManager.minimum_collateral_ratio(), - "E7" - ); - assertLt( - (_trove2.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove2.debt, - troveManager.minimum_collateral_ratio(), - "E8" - ); - - // Liquidate the first trove - vm.startPrank(liquidator); - uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; - _troveIdsToLiquidate[0] = _troveId1; - troveManager.liquidate_troves(_troveIdsToLiquidate); - vm.stopPrank(); - - // Check liquidator received no fee (all collateral goes to auction) - assertEq(collateralToken.balanceOf(liquidator), 0, "E9"); - - // Check auction 0 has all collateral from first trove - // uint256 _auctionId0 = 0; - { - uint256 _collateralInAuction = _collateralNeeded; - assertTrue(auction.is_active(0), "E10"); - assertEq(auction.get_available_amount(0), _collateralInAuction, "E11"); - assertEq(collateralToken.balanceOf(address(auction)), _collateralInAuction, "E12"); - - // Check auction starting price and minimum price after first liquidation - assertEq( - auction.auctions(0).startingPrice, - _collateralInAuction * _priceDropToBelowMCR18 * dutchDesk.liquidation_starting_price_buffer_percentage() / 1e18 - / COLLATERAL_TOKEN_PRECISION, - "E13" - ); - assertEq(auction.auctions(0).minimumPrice, _priceDropToBelowMCR18 * dutchDesk.liquidation_minimum_price_buffer_percentage() / WAD, "E14"); - } - - // Check first trove is liquidated - _trove1 = troveManager.troves(_troveId1); - assertEq(uint256(troveManager.troves(_troveId1).status), uint256(ITroveManager.Status.liquidated), "E15"); - - // Check second trove is still active - _trove2 = troveManager.troves(_troveId2); - assertEq(uint256(_trove2.status), uint256(ITroveManager.Status.active), "E16"); - - // Liquidate the second trove (before taking the first auction) - // In new architecture, this creates a separate auction with ID 1 - vm.startPrank(liquidator); - _troveIdsToLiquidate[0] = _troveId2; - troveManager.liquidate_troves(_troveIdsToLiquidate); - vm.stopPrank(); - - // Check liquidator received no fees (all collateral goes to auction) - assertEq(collateralToken.balanceOf(liquidator), 0, "E17"); - - // Check second trove is now liquidated - _trove2 = troveManager.troves(_troveId2); - assertEq(uint256(_trove2.status), uint256(ITroveManager.Status.liquidated), "E18"); - - // Check auction 1 has all collateral from second trove (separate auction) - // uint256 _auctionId1 = 1; - { - uint256 _collateralInAuction = _collateralNeeded; - assertTrue(auction.is_active(1), "E19"); - assertEq(auction.get_available_amount(1), _collateralInAuction, "E20"); - assertEq(collateralToken.balanceOf(address(auction)), _collateralInAuction * 2, "E21"); - - // Check auction 0 is still active with first trove's collateral - assertTrue(auction.is_active(0), "E22"); - assertEq(auction.get_available_amount(0), _collateralInAuction, "E23"); - } - - // Take both auctions - takeAuction(0); - takeAuction(1); - - // Both auctions should be empty now - assertEq(collateralToken.balanceOf(address(auction)), 0, "E24"); - assertFalse(auction.is_active(0), "E25"); - assertFalse(auction.is_active(1), "E26"); - - // Make sure lender got all the borrow tokens back + liquidation fees - assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt * 2, "E27"); - - // Make sure liquidator got all the collateral (fees + auction proceeds) - assertEq(collateralToken.balanceOf(liquidator), _collateralNeeded * 2, "E28"); - - // Check dutch desk is empty - assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E29"); - assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E30"); - - // Check global info - assertEq(troveManager.total_debt(), 0, "E31"); - assertEq(troveManager.total_weighted_debt(), 0, "E32"); - assertEq(troveManager.collateral_balance(), 0, "E33"); - } - - function test_liquidateTroves_emptyList() public { - // Make sure we always fail when no trove ids are passed - uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; - vm.expectRevert("!trove_ids"); - troveManager.liquidate_troves(_troveIdsToLiquidate); - } - - function test_liquidateTroves_nonExistentTrove( - uint256 _nonExistentTroveId - ) public { - _nonExistentTroveId = bound(_nonExistentTroveId, 1, type(uint256).max); - // Make sure we always fail when a non-existent trove is passed - uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; - _troveIdsToLiquidate[0] = _nonExistentTroveId; - vm.expectRevert("!active or zombie"); - troveManager.liquidate_troves(_troveIdsToLiquidate); - } - - function test_liquidateTroves_aboveMCR( - uint256 _amount - ) public { - _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); - - // Lend some from lender - mintAndDepositIntoLender(userLender, _amount); - - // Calculate how much collateral is needed for the borrow amount - uint256 _collateralNeeded = - (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - - // Open a trove - uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Make sure we cannot liquidate a trove that is above MCR - uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; - _troveIdsToLiquidate[0] = _troveId; - vm.expectRevert("!collateral_ratio"); - troveManager.liquidate_troves(_troveIdsToLiquidate); - } - - function test_liquidateGas() public { - uint256 _amount = troveManager.min_debt() * BORROW_TOKEN_PRECISION; - - mintAndDepositIntoLender(userLender, _amount); - - uint256 _collateralNeeded = - (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - - uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); - - // Drop price below MCR - ITroveManager.Trove memory _trove = troveManager.troves(_troveId); - uint256 _priceDropToBelowMCR = - troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); - uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); - - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); - vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - - uint256[MAX_LIQUIDATIONS] memory _troveIds; - _troveIds[0] = _troveId; - - uint256 _gasBefore = gasleft(); - vm.prank(liquidator); - troveManager.liquidate_troves(_troveIds); - uint256 _gasUsed = _gasBefore - gasleft(); - - console2.log("Gas used to liquidate 1 trove:", _gasUsed); - } - -} +// // SPDX-License-Identifier: MIT +// pragma solidity 0.8.23; + +// import "./Base.sol"; +// import {IPriceOracleNotScaled} from "./interfaces/IPriceOracleNotScaled.sol"; +// import {IPriceOracleScaled} from "./interfaces/IPriceOracleScaled.sol"; + +// contract LiquidateTests is Base { + +// function setUp() public override { +// Base.setUp(); + +// // Set `profitMaxUnlockTime` to 0 +// vm.prank(management); +// lender.setProfitMaxUnlockTime(0); + +// // Set fees to 0 +// vm.prank(management); +// lender.setPerformanceFee(0); +// } + +// // 1. lend +// // 2. borrow all available liquidity +// // 3. collateral price drops +// // 4. liquidate trove +// function test_liquidateTrove( +// uint256 _amount +// ) public { +// _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); + +// // Lend some from lender +// mintAndDepositIntoLender(userLender, _amount); + +// // Calculate how much collateral is needed for the borrow amount +// uint256 _collateralNeeded = +// (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + +// // Calculate expected debt (borrow amount + upfront fee) +// uint256 _expectedDebt = _amount + troveManager.get_upfront_fee(_amount, DEFAULT_ANNUAL_INTEREST_RATE); + +// // Open a trove +// uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + +// // Check trove info +// ITroveManager.Trove memory _trove = troveManager.troves(_troveId); +// assertEq(_trove.debt, _expectedDebt, "E0"); +// assertEq(_trove.collateral, _collateralNeeded, "E1"); +// assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E2"); +// assertEq(_trove.last_debt_update_time, block.timestamp, "E3"); +// assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E4"); +// assertEq(_trove.owner, userBorrower, "E5"); +// assertEq(_trove.pending_owner, address(0), "E6"); +// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E7"); +// assertApproxEqRel( +// (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, +// DEFAULT_TARGET_COLLATERAL_RATIO, +// 1e15, +// "E8" +// ); // 0.1% + +// // Check sorted troves +// assertFalse(sortedTroves.empty(), "E9"); +// assertEq(sortedTroves.size(), 1, "E10"); +// assertEq(sortedTroves.first(), _troveId, "E11"); +// assertEq(sortedTroves.last(), _troveId, "E12"); +// assertTrue(sortedTroves.contains(_troveId), "E13"); + +// // Check balances +// assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded, "E14"); +// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E15"); +// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E16"); +// assertEq(borrowToken.balanceOf(address(lender)), 0, "E17"); +// assertEq(borrowToken.balanceOf(userBorrower), _amount, "E18"); + +// // Check global info +// assertEq(troveManager.total_debt(), _expectedDebt, "E19"); +// assertEq(troveManager.total_weighted_debt(), _expectedDebt * DEFAULT_ANNUAL_INTEREST_RATE, "E20"); +// assertEq(troveManager.collateral_balance(), _collateralNeeded, "E21"); +// assertEq(troveManager.zombie_trove_id(), 0, "E22"); + +// // Check dutch desk is empty +// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E23"); +// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E24"); + +// // CR = (collateral * price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / debt +// // So price_at_MCR = MCR * debt * ORACLE_PRICE_SCALE / (collateral * BORROW_TOKEN_PRECISION) +// // We want to be 1% below MCR +// uint256 _priceDropToBelowMCR; +// if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { +// // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow +// _priceDropToBelowMCR = +// troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); +// } else { +// // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow +// _priceDropToBelowMCR = +// troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; +// } +// uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); + +// // Drop collateral price to put trove below MCR +// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); +// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); + +// // Make sure price actually dropped +// assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E25"); + +// // Calculate Trove's collateral ratio after price drop +// uint256 _troveCollateralRatioAfter = (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; + +// // Make sure Trove is below MCR +// assertLt(_troveCollateralRatioAfter, troveManager.minimum_collateral_ratio(), "E26"); + +// // Finally, liquidate the trove +// { +// vm.startPrank(liquidator); +// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; +// _troveIdsToLiquidate[0] = _troveId; +// troveManager.liquidate_troves(_troveIdsToLiquidate); +// vm.stopPrank(); +// } + +// // Check liquidator received no fee (all collateral goes to auction) +// assertEq(collateralToken.balanceOf(liquidator), 0, "E26a"); + +// // Check auction starting price and minimum price +// { +// uint256 _expectedStartingPrice = _collateralNeeded * _priceDropToBelowMCR18 * dutchDesk.liquidation_starting_price_buffer_percentage() +// / 1e18 / COLLATERAL_TOKEN_PRECISION; +// assertEq(auction.auctions(0).startingPrice, _expectedStartingPrice, "E27"); +// uint256 _expectedMinimumPrice = _priceDropToBelowMCR18 * dutchDesk.liquidation_minimum_price_buffer_percentage() / WAD; +// assertEq(auction.auctions(0).minimumPrice, _expectedMinimumPrice, "E28"); +// } + +// // Take the auction +// takeAuction(0); + +// // Make sure lender got all the borrow tokens back + liquidation fee +// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E29"); + +// // Make sure liquidator got the collateral +// assertEq(collateralToken.balanceOf(liquidator), _collateralNeeded, "E30"); + +// // Check everything again + +// // Check trove info +// _trove = troveManager.troves(_troveId); +// assertEq(_trove.debt, 0, "E31"); +// assertEq(_trove.collateral, 0, "E32"); +// assertEq(_trove.annual_interest_rate, 0, "E33"); +// assertEq(_trove.last_debt_update_time, 0, "E34"); +// assertEq(_trove.last_interest_rate_adj_time, 0, "E35"); +// assertEq(_trove.owner, address(0), "E36"); +// assertEq(_trove.pending_owner, address(0), "E37"); +// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E38"); + +// // Check sorted troves +// assertTrue(sortedTroves.empty(), "E39"); +// assertEq(sortedTroves.size(), 0, "E40"); +// assertEq(sortedTroves.first(), 0, "E41"); +// assertEq(sortedTroves.last(), 0, "E42"); +// assertFalse(sortedTroves.contains(_troveId), "E43"); + +// // Check balances +// assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E44"); +// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E45"); +// assertEq(collateralToken.balanceOf(address(userBorrower)), 0, "E46"); +// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E47"); +// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E48"); +// assertEq(borrowToken.balanceOf(userBorrower), _amount, "E49"); + +// // Check global info +// assertEq(troveManager.total_debt(), 0, "E50"); +// assertEq(troveManager.total_weighted_debt(), 0, "E51"); +// assertEq(troveManager.collateral_balance(), 0, "E52"); +// assertEq(troveManager.zombie_trove_id(), 0, "E53"); + +// // Check dutch desk is empty +// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E54"); +// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E55"); +// } + +// // 1. lend +// // 2. borrow half of available liquidity from 1st borrower +// // 3. borrow half of available liquidity from 2nd borrower +// // 4. collateral price drops +// // 5. liquidate both troves +// function test_liquidateTroves( +// uint256 _amount +// ) public { +// _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); + +// // Lend some from lender +// mintAndDepositIntoLender(userLender, _amount); + +// uint256 _halfAmount = _amount / 2; + +// // Calculate how much collateral is needed for the borrow amount +// uint256 _collateralNeeded = +// (_halfAmount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + +// // Calculate expected debt (borrow amount + upfront fee) +// uint256 _expectedDebt = _halfAmount + troveManager.get_upfront_fee(_halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + +// // Open a trove for the first borrower +// uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + +// // Check trove info +// ITroveManager.Trove memory _trove = troveManager.troves(_troveId); +// assertEq(_trove.debt, _expectedDebt, "E0"); +// assertEq(_trove.collateral, _collateralNeeded, "E1"); +// assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E2"); +// assertEq(_trove.last_debt_update_time, block.timestamp, "E3"); +// assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E4"); +// assertEq(_trove.owner, userBorrower, "E5"); +// assertEq(_trove.pending_owner, address(0), "E6"); +// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E7"); +// assertApproxEqRel( +// (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, +// DEFAULT_TARGET_COLLATERAL_RATIO, +// 1e15, +// "E8" +// ); // 0.1% + +// // Check sorted troves +// assertFalse(sortedTroves.empty(), "E9"); +// assertEq(sortedTroves.size(), 1, "E10"); +// assertEq(sortedTroves.first(), _troveId, "E11"); +// assertEq(sortedTroves.last(), _troveId, "E12"); +// assertTrue(sortedTroves.contains(_troveId), "E13"); + +// // Check balances +// assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded, "E14"); +// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E15"); +// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E16"); +// assertApproxEqAbs(borrowToken.balanceOf(address(lender)), _halfAmount, 1, "E17"); +// assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E18"); + +// // Check global info +// assertEq(troveManager.total_debt(), _expectedDebt, "E19"); +// assertEq(troveManager.total_weighted_debt(), _expectedDebt * DEFAULT_ANNUAL_INTEREST_RATE, "E20"); +// assertEq(troveManager.collateral_balance(), _collateralNeeded, "E21"); +// assertEq(troveManager.zombie_trove_id(), 0, "E22"); + +// // Check dutch desk is empty +// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E23"); +// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E24"); + +// // Open a trove for the second borrower +// uint256 _anotherTroveId = mintAndOpenTrove(anotherUserBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + +// // Check trove info +// _trove = troveManager.troves(_anotherTroveId); +// assertEq(_trove.debt, _expectedDebt, "E25"); +// assertEq(_trove.collateral, _collateralNeeded, "E26"); +// assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E27"); +// assertEq(_trove.last_debt_update_time, block.timestamp, "E28"); +// assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E29"); +// assertEq(_trove.owner, anotherUserBorrower, "E30"); +// assertEq(_trove.pending_owner, address(0), "E31"); +// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E32"); +// assertApproxEqRel( +// (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, +// DEFAULT_TARGET_COLLATERAL_RATIO, +// 1e15, +// "E33" +// ); // 0.1% + +// // Check sorted troves +// assertFalse(sortedTroves.empty(), "E34"); +// assertEq(sortedTroves.size(), 2, "E35"); +// assertEq(sortedTroves.first(), _troveId, "E36"); +// assertEq(sortedTroves.last(), _anotherTroveId, "E37"); +// assertTrue(sortedTroves.contains(_troveId), "E38"); +// assertTrue(sortedTroves.contains(_anotherTroveId), "E39"); + +// // Check balances +// assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded * 2, "E40"); +// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E41"); +// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E42"); +// assertApproxEqAbs(borrowToken.balanceOf(address(lender)), 0, 1, "E43"); +// assertEq(borrowToken.balanceOf(anotherUserBorrower), _halfAmount, "E44"); + +// // Check global info +// assertEq(troveManager.total_debt(), _expectedDebt * 2, "E45"); +// assertEq(troveManager.total_weighted_debt(), _expectedDebt * 2 * DEFAULT_ANNUAL_INTEREST_RATE, "E46"); +// assertEq(troveManager.collateral_balance(), _collateralNeeded * 2, "E47"); +// assertEq(troveManager.zombie_trove_id(), 0, "E48"); + +// // Check dutch desk is empty +// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E49"); +// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E50"); + +// // CR = (collateral * price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / debt +// // So price_at_MCR = MCR * debt * ORACLE_PRICE_SCALE / (collateral * BORROW_TOKEN_PRECISION) +// // We want to be 1% below MCR +// uint256 _priceDropToBelowMCR; +// if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { +// // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow +// _priceDropToBelowMCR = +// troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); +// } else { +// // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow +// _priceDropToBelowMCR = +// troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; +// } +// uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); + +// // Drop collateral price to put trove below MCR +// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); +// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); + +// // Make sure price actually dropped +// assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E51"); + +// // Calculate Trove's collateral ratio after price drop +// uint256 _troveCollateralRatioAfter = (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; + +// // Make sure Trove is below MCR +// assertLt(_troveCollateralRatioAfter, troveManager.minimum_collateral_ratio(), "E52"); + +// // Finally, liquidate the troves +// { +// vm.startPrank(liquidator); +// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; +// _troveIdsToLiquidate[0] = _troveId; +// _troveIdsToLiquidate[1] = _anotherTroveId; +// troveManager.liquidate_troves(_troveIdsToLiquidate); +// vm.stopPrank(); +// } + +// // Check liquidator received no fee (all collateral goes to auction) +// assertEq(collateralToken.balanceOf(liquidator), 0, "E53"); + +// // Check auction starting price and minimum price +// // Starting price = available * price * STARTING_PRICE_BUFFER_PERCENTAGE / 1e18 / COLLATERAL_TOKEN_PRECISION +// // Note: both troves liquidated in same tx, so all collateral goes to auction +// { +// uint256 _collateralToSell = _collateralNeeded * 2; +// uint256 _expectedStartingPrice = _collateralToSell * _priceDropToBelowMCR18 * dutchDesk.liquidation_starting_price_buffer_percentage() +// / 1e18 / COLLATERAL_TOKEN_PRECISION; +// assertEq(auction.auctions(0).startingPrice, _expectedStartingPrice, "E54"); +// uint256 _expectedMinimumPrice = _priceDropToBelowMCR18 * dutchDesk.liquidation_minimum_price_buffer_percentage() / WAD; +// assertEq(auction.auctions(0).minimumPrice, _expectedMinimumPrice, "E55"); +// } + +// // Take the auction +// takeAuction(0); + +// // Make sure lender got all the borrow tokens back + liquidation fee +// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt * 2, "E56"); + +// // Make sure liquidator got the collateral +// assertEq(collateralToken.balanceOf(liquidator), _collateralNeeded * 2, "E57"); + +// // Check everything again + +// // Check trove info +// _trove = troveManager.troves(_troveId); +// assertEq(_trove.debt, 0, "E58"); +// assertEq(_trove.collateral, 0, "E59"); +// assertEq(_trove.annual_interest_rate, 0, "E60"); +// assertEq(_trove.last_debt_update_time, 0, "E61"); +// assertEq(_trove.last_interest_rate_adj_time, 0, "E62"); +// assertEq(_trove.owner, address(0), "E63"); +// assertEq(_trove.pending_owner, address(0), "E64"); +// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E65"); + +// // Check sorted troves +// assertTrue(sortedTroves.empty(), "E66"); +// assertEq(sortedTroves.size(), 0, "E67"); +// assertEq(sortedTroves.first(), 0, "E68"); +// assertEq(sortedTroves.last(), 0, "E69"); +// assertFalse(sortedTroves.contains(_troveId), "E70"); + +// // Check balances +// assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E71"); +// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E72"); +// assertEq(collateralToken.balanceOf(address(userBorrower)), 0, "E73"); +// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E74"); +// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E75"); +// assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E76"); + +// // Check global info +// assertEq(troveManager.total_debt(), 0, "E77"); +// assertEq(troveManager.total_weighted_debt(), 0, "E78"); +// assertEq(troveManager.collateral_balance(), 0, "E79"); +// assertEq(troveManager.zombie_trove_id(), 0, "E80"); + +// // Check dutch desk is empty +// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E81"); +// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E82"); + +// // Check everything again for the second trove + +// // Check trove info +// _trove = troveManager.troves(_anotherTroveId); +// assertEq(_trove.debt, 0, "E83"); +// assertEq(_trove.collateral, 0, "E84"); +// assertEq(_trove.annual_interest_rate, 0, "E85"); +// assertEq(_trove.last_debt_update_time, 0, "E86"); +// assertEq(_trove.last_interest_rate_adj_time, 0, "E87"); +// assertEq(_trove.owner, address(0), "E88"); +// assertEq(_trove.pending_owner, address(0), "E89"); +// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E90"); + +// // Check sorted troves +// assertTrue(sortedTroves.empty(), "E91"); +// assertEq(sortedTroves.size(), 0, "E92"); +// assertEq(sortedTroves.first(), 0, "E93"); +// assertEq(sortedTroves.last(), 0, "E94"); +// assertFalse(sortedTroves.contains(_anotherTroveId), "E95"); + +// // Check balances +// assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E96"); +// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E97"); +// assertEq(collateralToken.balanceOf(address(userBorrower)), 0, "E98"); +// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E99"); +// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E100"); +// assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E101"); + +// // Check global info +// assertEq(troveManager.total_debt(), 0, "E102"); +// assertEq(troveManager.total_weighted_debt(), 0, "E103"); +// assertEq(troveManager.collateral_balance(), 0, "E104"); +// assertEq(troveManager.zombie_trove_id(), 0, "E105"); + +// // Check dutch desk is empty +// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E106"); +// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E107"); +// } + +// // 1. lend +// // 2. open 2 troves +// // 3. collateral price drops +// // 4. liquidate first trove +// // 5. liquidate second trove (before taking the first auction) +// // 6. take the auction (should have collateral from both troves - DutchDesk sweeps + settles + re-kicks with all collateral) +// function test_liquidateTroves_sequentialLiquidations( +// uint256 _amount +// ) public { +// _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); + +// // Lend some from lender +// mintAndDepositIntoLender(userLender, _amount); + +// uint256 _halfAmount = _amount / 2; + +// // Calculate how much collateral is needed for the borrow amount +// uint256 _collateralNeeded = +// (_halfAmount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + +// // Calculate expected debt (borrow amount + upfront fee) +// uint256 _expectedDebt = _halfAmount + troveManager.get_upfront_fee(_halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + +// // Open first trove +// uint256 _troveId1 = mintAndOpenTrove(userBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + +// // Open second trove +// uint256 _troveId2 = mintAndOpenTrove(anotherUserBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + +// // Check trove info for first trove +// ITroveManager.Trove memory _trove1 = troveManager.troves(_troveId1); +// assertEq(_trove1.debt, _expectedDebt, "E0"); +// assertEq(_trove1.collateral, _collateralNeeded, "E1"); + +// // Check trove info for second trove +// ITroveManager.Trove memory _trove2 = troveManager.troves(_troveId2); +// assertEq(_trove2.debt, _expectedDebt, "E2"); +// assertEq(_trove2.collateral, _collateralNeeded, "E3"); + +// // Check dutch desk is empty +// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E4"); +// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E5"); + +// // CR = collateral * price / debt, so price_at_MCR = MCR * debt / collateral +// // We want to be 1% below MCR +// uint256 _priceDropToBelowMCR; +// if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { +// // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow +// _priceDropToBelowMCR = troveManager.minimum_collateral_ratio() * _trove1.debt * ORACLE_PRICE_SCALE * 99 +// / (100 * _trove1.collateral * BORROW_TOKEN_PRECISION); +// } else { +// // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow +// _priceDropToBelowMCR = +// troveManager.minimum_collateral_ratio() * _trove1.debt * 99 / 100 / _trove1.collateral * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION; +// } +// uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); + +// // Drop collateral price to put both troves below MCR +// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); +// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); + +// // Make sure price actually dropped +// assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E6"); + +// // Make sure both troves are below MCR +// assertLt( +// (_trove1.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove1.debt, +// troveManager.minimum_collateral_ratio(), +// "E7" +// ); +// assertLt( +// (_trove2.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove2.debt, +// troveManager.minimum_collateral_ratio(), +// "E8" +// ); + +// // Liquidate the first trove +// vm.startPrank(liquidator); +// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; +// _troveIdsToLiquidate[0] = _troveId1; +// troveManager.liquidate_troves(_troveIdsToLiquidate); +// vm.stopPrank(); + +// // Check liquidator received no fee (all collateral goes to auction) +// assertEq(collateralToken.balanceOf(liquidator), 0, "E9"); + +// // Check auction 0 has all collateral from first trove +// // uint256 _auctionId0 = 0; +// { +// uint256 _collateralInAuction = _collateralNeeded; +// assertTrue(auction.is_active(0), "E10"); +// assertEq(auction.get_available_amount(0), _collateralInAuction, "E11"); +// assertEq(collateralToken.balanceOf(address(auction)), _collateralInAuction, "E12"); + +// // Check auction starting price and minimum price after first liquidation +// assertEq( +// auction.auctions(0).startingPrice, +// _collateralInAuction * _priceDropToBelowMCR18 * dutchDesk.liquidation_starting_price_buffer_percentage() / 1e18 +// / COLLATERAL_TOKEN_PRECISION, +// "E13" +// ); +// assertEq(auction.auctions(0).minimumPrice, _priceDropToBelowMCR18 * dutchDesk.liquidation_minimum_price_buffer_percentage() / WAD, "E14"); +// } + +// // Check first trove is liquidated +// _trove1 = troveManager.troves(_troveId1); +// assertEq(uint256(troveManager.troves(_troveId1).status), uint256(ITroveManager.Status.liquidated), "E15"); + +// // Check second trove is still active +// _trove2 = troveManager.troves(_troveId2); +// assertEq(uint256(_trove2.status), uint256(ITroveManager.Status.active), "E16"); + +// // Liquidate the second trove (before taking the first auction) +// // In new architecture, this creates a separate auction with ID 1 +// vm.startPrank(liquidator); +// _troveIdsToLiquidate[0] = _troveId2; +// troveManager.liquidate_troves(_troveIdsToLiquidate); +// vm.stopPrank(); + +// // Check liquidator received no fees (all collateral goes to auction) +// assertEq(collateralToken.balanceOf(liquidator), 0, "E17"); + +// // Check second trove is now liquidated +// _trove2 = troveManager.troves(_troveId2); +// assertEq(uint256(_trove2.status), uint256(ITroveManager.Status.liquidated), "E18"); + +// // Check auction 1 has all collateral from second trove (separate auction) +// // uint256 _auctionId1 = 1; +// { +// uint256 _collateralInAuction = _collateralNeeded; +// assertTrue(auction.is_active(1), "E19"); +// assertEq(auction.get_available_amount(1), _collateralInAuction, "E20"); +// assertEq(collateralToken.balanceOf(address(auction)), _collateralInAuction * 2, "E21"); + +// // Check auction 0 is still active with first trove's collateral +// assertTrue(auction.is_active(0), "E22"); +// assertEq(auction.get_available_amount(0), _collateralInAuction, "E23"); +// } + +// // Take both auctions +// takeAuction(0); +// takeAuction(1); + +// // Both auctions should be empty now +// assertEq(collateralToken.balanceOf(address(auction)), 0, "E24"); +// assertFalse(auction.is_active(0), "E25"); +// assertFalse(auction.is_active(1), "E26"); + +// // Make sure lender got all the borrow tokens back + liquidation fees +// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt * 2, "E27"); + +// // Make sure liquidator got all the collateral (fees + auction proceeds) +// assertEq(collateralToken.balanceOf(liquidator), _collateralNeeded * 2, "E28"); + +// // Check dutch desk is empty +// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E29"); +// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E30"); + +// // Check global info +// assertEq(troveManager.total_debt(), 0, "E31"); +// assertEq(troveManager.total_weighted_debt(), 0, "E32"); +// assertEq(troveManager.collateral_balance(), 0, "E33"); +// } + +// function test_liquidateTroves_emptyList() public { +// // Make sure we always fail when no trove ids are passed +// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; +// vm.expectRevert("!trove_ids"); +// troveManager.liquidate_troves(_troveIdsToLiquidate); +// } + +// function test_liquidateTroves_nonExistentTrove( +// uint256 _nonExistentTroveId +// ) public { +// _nonExistentTroveId = bound(_nonExistentTroveId, 1, type(uint256).max); +// // Make sure we always fail when a non-existent trove is passed +// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; +// _troveIdsToLiquidate[0] = _nonExistentTroveId; +// vm.expectRevert("!active or zombie"); +// troveManager.liquidate_troves(_troveIdsToLiquidate); +// } + +// function test_liquidateTroves_aboveMCR( +// uint256 _amount +// ) public { +// _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); + +// // Lend some from lender +// mintAndDepositIntoLender(userLender, _amount); + +// // Calculate how much collateral is needed for the borrow amount +// uint256 _collateralNeeded = +// (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + +// // Open a trove +// uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + +// // Make sure we cannot liquidate a trove that is above MCR +// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; +// _troveIdsToLiquidate[0] = _troveId; +// vm.expectRevert("!collateral_ratio"); +// troveManager.liquidate_troves(_troveIdsToLiquidate); +// } + +// function test_liquidateGas() public { +// uint256 _amount = troveManager.min_debt() * BORROW_TOKEN_PRECISION; + +// mintAndDepositIntoLender(userLender, _amount); + +// uint256 _collateralNeeded = +// (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + +// uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + +// // Drop price below MCR +// ITroveManager.Trove memory _trove = troveManager.troves(_troveId); +// uint256 _priceDropToBelowMCR = +// troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); +// uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); + +// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); +// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); + +// uint256[MAX_LIQUIDATIONS] memory _troveIds; +// _troveIds[0] = _troveId; + +// uint256 _gasBefore = gasleft(); +// vm.prank(liquidator); +// troveManager.liquidate_troves(_troveIds); +// uint256 _gasUsed = _gasBefore - gasleft(); + +// console2.log("Gas used to liquidate 1 trove:", _gasUsed); +// } + +// } diff --git a/test/OpenTrove.t.sol b/test/OpenTrove.t.sol index 0e66dd7..b9bfc24 100644 --- a/test/OpenTrove.t.sol +++ b/test/OpenTrove.t.sol @@ -1,9 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {IPriceOracleNotScaled} from "./interfaces/IPriceOracleNotScaled.sol"; -import {IPriceOracleScaled} from "./interfaces/IPriceOracleScaled.sol"; - import "./Base.sol"; contract OpenTroveTests is Base { @@ -182,19 +179,14 @@ contract OpenTroveTests is Base { // Check starting price is set correctly (with buffer) assertApproxEqAbs( - auction.auctions(_auctionId).startingPrice, - _auctionAvailable * priceOracle.get_price(false) / WAD * dutchDesk.redemption_starting_price_buffer_percentage() - / COLLATERAL_TOKEN_PRECISION, + auction.auctions(_auctionId).starting_price, + _auctionAvailable * priceOracle.get_price(false) / WAD * dutchDesk.starting_price_buffer_percentage() / COLLATERAL_TOKEN_PRECISION, 3, "E3" ); // Check minimum price is set correctly (with buffer) - assertEq( - auction.auctions(_auctionId).minimumPrice, - priceOracle.get_price(false) * dutchDesk.redemption_minimum_price_buffer_percentage() / WAD, - "E4" - ); + assertEq(auction.auctions(_auctionId).minimum_price, priceOracle.get_price(false) * dutchDesk.minimum_price_buffer_percentage() / WAD, "E4"); // Take the auction takeAuction(_auctionId); @@ -314,15 +306,14 @@ contract OpenTroveTests is Base { // Check starting price is set correctly (with buffer) assertApproxEqAbs( - auction.auctions(0).startingPrice, - _auctionAvailable * priceOracle.get_price(false) / WAD * dutchDesk.redemption_starting_price_buffer_percentage() - / COLLATERAL_TOKEN_PRECISION, + auction.auctions(0).starting_price, + _auctionAvailable * priceOracle.get_price(false) / WAD * dutchDesk.starting_price_buffer_percentage() / COLLATERAL_TOKEN_PRECISION, 3, "E3" ); // Check minimum price is set correctly (with buffer) - assertEq(auction.auctions(0).minimumPrice, priceOracle.get_price(false) * dutchDesk.redemption_minimum_price_buffer_percentage() / WAD, "E4"); + assertEq(auction.auctions(0).minimum_price, priceOracle.get_price(false) * dutchDesk.minimum_price_buffer_percentage() / WAD, "E4"); // Take the auction takeAuction(0); @@ -452,15 +443,14 @@ contract OpenTroveTests is Base { // Check starting price is set correctly (with buffer) assertApproxEqAbs( - auction.auctions(0).startingPrice, - _auctionAvailable * priceOracle.get_price(false) / WAD * dutchDesk.redemption_starting_price_buffer_percentage() - / COLLATERAL_TOKEN_PRECISION, + auction.auctions(0).starting_price, + _auctionAvailable * priceOracle.get_price(false) / WAD * dutchDesk.starting_price_buffer_percentage() / COLLATERAL_TOKEN_PRECISION, 3, "E3" ); // Check minimum price is set correctly (with buffer) - assertEq(auction.auctions(0).minimumPrice, priceOracle.get_price(false) * dutchDesk.redemption_minimum_price_buffer_percentage() / WAD, "E4"); + assertEq(auction.auctions(0).minimum_price, priceOracle.get_price(false) * dutchDesk.minimum_price_buffer_percentage() / WAD, "E4"); // Take the auction takeAuction(0); diff --git a/test/TransferOwnership.t.sol b/test/TransferOwnership.t.sol index 2df2224..c460718 100644 --- a/test/TransferOwnership.t.sol +++ b/test/TransferOwnership.t.sol @@ -51,6 +51,7 @@ contract TransferOwnershipTests is Base { uint256 _amount, address _newOwner ) public { + vm.assume(_newOwner != userLender); _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); // Start the ownership transfer process diff --git a/test/TroveManager.t.sol b/test/TroveManager.t.sol index 93073a1..97dcee2 100644 --- a/test/TroveManager.t.sol +++ b/test/TroveManager.t.sol @@ -36,15 +36,15 @@ contract TroveManagerTests is Base { troveManager.initialize( ITroveManager.InitializeParams({ lender: address(lender), - dutchDesk: address(dutchDesk), - priceOracle: address(priceOracle), - sortedTroves: address(sortedTroves), - borrowToken: address(borrowToken), - collateralToken: address(collateralToken), - minimumDebt: minimumDebt, - minimumCollateralRatio: minimumCollateralRatio, - upfrontInterestPeriod: upfrontInterestPeriod, - interestRateAdjCooldown: interestRateAdjCooldown + dutch_desk: address(dutchDesk), + price_oracle: address(priceOracle), + sorted_troves: address(sortedTroves), + borrow_token: address(borrowToken), + collateral_token: address(collateralToken), + minimum_debt: minimumDebt, + minimum_collateral_ratio: minimumCollateralRatio, + upfront_interest_period: upfrontInterestPeriod, + interest_rate_adj_cooldown: interestRateAdjCooldown }) ); } diff --git a/test/interfaces/IAuction.sol b/test/interfaces/IAuction.sol index caa40e5..01320c8 100644 --- a/test/interfaces/IAuction.sol +++ b/test/interfaces/IAuction.sol @@ -8,25 +8,24 @@ interface IAuction { // ============================================================================================ struct AuctionInfo { - uint256 kickTimestamp; - uint256 initialAmount; - uint256 currentAmount; - uint256 maximumAmount; - uint256 amountReceived; - uint256 startingPrice; - uint256 minimumPrice; + uint256 kick_timestamp; + uint256 initial_amount; + uint256 current_amount; + uint256 maximum_amount; + uint256 amount_received; + uint256 starting_price; + uint256 minimum_price; address receiver; - address surplusReceiver; - bool isLiquidation; + address surplus_receiver; } struct InitializeParams { address papi; - address buyToken; - address sellToken; - uint256 stepDuration; - uint256 stepDecayRate; - uint256 auctionLength; + address buy_token; + address sell_token; + uint256 step_duration; + uint256 step_decay_rate; + uint256 auction_length; } // ============================================================================================ @@ -48,7 +47,6 @@ interface IAuction { function auction_length() external view returns (uint256); // Accounting - function liquidation_auctions() external view returns (uint256); function auctions( uint256 auctionId ) external view returns (AuctionInfo memory); @@ -83,7 +81,6 @@ interface IAuction { function is_active( uint256 auction_id ) external view returns (bool); - function is_ongoing_liquidation_auction() external view returns (bool); // ============================================================================================ // Kick @@ -96,8 +93,7 @@ interface IAuction { uint256 starting_price, uint256 minimum_price, address receiver, - address surplus_receiver, - bool is_liquidation + address surplus_receiver ) external; function re_kick( diff --git a/test/interfaces/IDutchDesk.sol b/test/interfaces/IDutchDesk.sol index 9cbd1e0..1e3632d 100644 --- a/test/interfaces/IDutchDesk.sol +++ b/test/interfaces/IDutchDesk.sol @@ -8,17 +8,15 @@ interface IDutchDesk { // ============================================================================================ struct InitializeParams { - address troveManager; + address trove_manager; address lender; - address priceOracle; + address price_oracle; address auction; - address borrowToken; - address collateralToken; - uint256 redemptionMinimumPriceBufferPercentage; - uint256 redemptionStartingPriceBufferPercentage; - uint256 redemptionReKickStartingPriceBufferPercentage; - uint256 liquidationMinimumPriceBufferPercentage; - uint256 liquidationStartingPriceBufferPercentage; + address borrow_token; + address collateral_token; + uint256 minimum_price_buffer_percentage; + uint256 starting_price_buffer_percentage; + uint256 re_kick_starting_price_buffer_percentage; } // ============================================================================================ @@ -36,11 +34,9 @@ interface IDutchDesk { // Parameters function collateral_token_precision() external view returns (uint256); - function redemption_minimum_price_buffer_percentage() external view returns (uint256); - function redemption_starting_price_buffer_percentage() external view returns (uint256); - function redemption_re_kick_starting_price_buffer_percentage() external view returns (uint256); - function liquidation_minimum_price_buffer_percentage() external view returns (uint256); - function liquidation_starting_price_buffer_percentage() external view returns (uint256); + function minimum_price_buffer_percentage() external view returns (uint256); + function starting_price_buffer_percentage() external view returns (uint256); + function re_kick_starting_price_buffer_percentage() external view returns (uint256); // Accounting function nonce() external view returns (uint256); @@ -60,8 +56,7 @@ interface IDutchDesk { function kick( uint256 kick_amount, uint256 maximum_amount, - address receiver, - bool is_liquidation + address receiver ) external; function re_kick( diff --git a/test/interfaces/ITroveManager.sol b/test/interfaces/ITroveManager.sol index 61c282c..24314ad 100644 --- a/test/interfaces/ITroveManager.sol +++ b/test/interfaces/ITroveManager.sol @@ -37,15 +37,15 @@ interface ITroveManager { struct InitializeParams { address lender; - address dutchDesk; - address priceOracle; - address sortedTroves; - address borrowToken; - address collateralToken; - uint256 minimumDebt; - uint256 minimumCollateralRatio; - uint256 upfrontInterestPeriod; - uint256 interestRateAdjCooldown; + address dutch_desk; + address price_oracle; + address sorted_troves; + address borrow_token; + address collateral_token; + uint256 minimum_debt; + uint256 minimum_collateral_ratio; + uint256 upfront_interest_period; + uint256 interest_rate_adj_cooldown; } // ============================================================================================ @@ -182,9 +182,12 @@ interface ITroveManager { // Liquidate trove // ============================================================================================ - function liquidate_troves( - uint256[20] calldata trove_ids - ) external; + function liquidate_trove( + uint256 trove_id, + uint256 max_amount, + address receiver, + bytes calldata data + ) external returns (uint256); // ============================================================================================ // Redeem From 122d275ccb2793bb57b1e493f27a507df3e22c95 Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Sat, 14 Feb 2026 17:09:53 -0300 Subject: [PATCH 2/9] fix: use the cached auction instance --- src/dutch_desk.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dutch_desk.vy b/src/dutch_desk.vy index 550719d..06fcd5e 100644 --- a/src/dutch_desk.vy +++ b/src/dutch_desk.vy @@ -166,7 +166,7 @@ def re_kick(auction_id: uint256): auction: IAuction = self.auction # Get the auction info - auction_info: IAuction.AuctionInfo = staticcall self.auction.auctions(auction_id) + auction_info: IAuction.AuctionInfo = staticcall auction.auctions(auction_id) # Get new starting and minimum prices starting_price: uint256 = 0 From b6a18a639ff696872bc1568ea43aaa75b229e0c5 Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Sat, 14 Feb 2026 17:12:06 -0300 Subject: [PATCH 3/9] chore: small doc cleanup --- src/trove_manager.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trove_manager.vy b/src/trove_manager.vy index c594bcf..50a6f60 100644 --- a/src/trove_manager.vy +++ b/src/trove_manager.vy @@ -967,7 +967,7 @@ def liquidate_trove( # Cache the Trove info trove: Trove = self.troves[trove_id] - # Cache if the Trove is active + # Check if the Trove is active and cache the result is_active: bool = trove.status == Status.ACTIVE # Make sure the Trove is active or zombie From 6985311fb9f1998fdbfd9471a088b29edd04b65c Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Sun, 15 Feb 2026 19:44:59 -0300 Subject: [PATCH 4/9] feat: add dynamic liquidation fee --- script/interfaces/ICatFactory.sol | 3 + src/factory.vy | 23 +- src/interfaces/ITroveManager.vyi | 3 + src/trove_manager.vy | 192 +++- test/Base.sol | 48 +- test/Gas.t.sol | 40 - test/Liquidate.t.sol | 1441 ++++++++++++++++------------- test/TroveManager.t.sol | 24 +- test/interfaces/ITroveManager.sol | 6 + test/mocks/LiquidatorMock.sol | 41 + 10 files changed, 1057 insertions(+), 764 deletions(-) create mode 100644 test/mocks/LiquidatorMock.sol diff --git a/script/interfaces/ICatFactory.sol b/script/interfaces/ICatFactory.sol index f0339d9..fcc16d2 100644 --- a/script/interfaces/ICatFactory.sol +++ b/script/interfaces/ICatFactory.sol @@ -15,6 +15,9 @@ interface ICatFactory { address performance_fee_recipient; uint256 minimum_debt; uint256 minimum_collateral_ratio; + uint256 max_penalty_collateral_ratio; + uint256 min_liquidation_fee; + uint256 max_liquidation_fee; uint256 upfront_interest_period; uint256 interest_rate_adj_cooldown; uint256 minimum_price_buffer_percentage; diff --git a/src/factory.vy b/src/factory.vy index 02e4396..ef9084d 100644 --- a/src/factory.vy +++ b/src/factory.vy @@ -42,7 +42,10 @@ struct DeployParams: management: address # address of the management performance_fee_recipient: address # address of the performance fee recipient minimum_debt: uint256 # minimum borrowable amount, e.g., `500 * borrow_token_precision` for 500 tokens - minimum_collateral_ratio: uint256 # minimum CR to avoid liquidation, e.g., `110 * one_pct` for 110% + minimum_collateral_ratio: uint256 # minimum CR to avoid liquidation, e.g., `110` for 110% + max_penalty_collateral_ratio: uint256 # CR at which max liquidation fee applies, e.g., `105` for 105% + min_liquidation_fee: uint256 # minimum liquidation fee in hundredths of a percent, e.g., `50` for 0.5% + max_liquidation_fee: uint256 # maximum liquidation fee in hundredths of a percent, e.g., `500` for 5% upfront_interest_period: uint256 # duration for upfront interest charges, e.g., `7 * 24 * 60 * 60` for 7 days interest_rate_adj_cooldown: uint256 # cooldown between rate adjustments, e.g., `7 * 24 * 60 * 60` for 7 days minimum_price_buffer_percentage: uint256 # auction minimum price buffer, e.g. `WAD - 5 * 10 ** 16` for 5% below oracle price @@ -69,10 +72,6 @@ LENDER_FACTORY: public(immutable(ILenderFactory)) # Version VERSION: public(constant(String[28])) = "1.0.0" -# Utils -_WAD: constant(uint256) = 10 ** 18 -_MAX_TOKEN_DECIMALS: constant(uint256) = 18 - # ============================================================================================ # Constructor @@ -118,17 +117,6 @@ def deploy(params: DeployParams) -> (address, address, address, address, address @return auction Address of the deployed Auction contract @return lender Address of the deployed Lender contract """ - # Make sure borrow and collateral tokens are different - assert params.borrow_token != params.collateral_token, "!tokens" - - # Borrow token cannot have more than 18 decimals - borrow_token_decimals: uint256 = convert(staticcall IERC20Detailed(params.borrow_token).decimals(), uint256) - assert borrow_token_decimals <= _MAX_TOKEN_DECIMALS, "!borrow_token_decimals" - - # Collateral token cannot have more than 18 decimals - collateral_token_decimals: uint256 = convert(staticcall IERC20Detailed(params.collateral_token).decimals(), uint256) - assert collateral_token_decimals <= _MAX_TOKEN_DECIMALS, "!collateral_token_decimals" - # Compute the salt value salt: bytes32 = keccak256(abi_encode(msg.sender, params.salt, params.collateral_token, params.borrow_token)) @@ -168,6 +156,9 @@ def deploy(params: DeployParams) -> (address, address, address, address, address collateral_token=params.collateral_token, minimum_debt=params.minimum_debt, minimum_collateral_ratio=params.minimum_collateral_ratio, + max_penalty_collateral_ratio=params.max_penalty_collateral_ratio, + min_liquidation_fee=params.min_liquidation_fee, + max_liquidation_fee=params.max_liquidation_fee, upfront_interest_period=params.upfront_interest_period, interest_rate_adj_cooldown=params.interest_rate_adj_cooldown, )) diff --git a/src/interfaces/ITroveManager.vyi b/src/interfaces/ITroveManager.vyi index 3f65c75..556e581 100644 --- a/src/interfaces/ITroveManager.vyi +++ b/src/interfaces/ITroveManager.vyi @@ -38,6 +38,9 @@ struct InitializeParams: collateral_token: address minimum_debt: uint256 minimum_collateral_ratio: uint256 + max_penalty_collateral_ratio: uint256 + min_liquidation_fee: uint256 + max_liquidation_fee: uint256 upfront_interest_period: uint256 interest_rate_adj_cooldown: uint256 diff --git a/src/trove_manager.vy b/src/trove_manager.vy index 50a6f60..b236a7b 100644 --- a/src/trove_manager.vy +++ b/src/trove_manager.vy @@ -88,6 +88,7 @@ event LiquidateTrove: liquidator: indexed(address) collateral_amount: uint256 debt_amount: uint256 + is_full_liquidation: bool event RedeemTrove: trove_id: indexed(uint256) @@ -139,6 +140,9 @@ struct InitializeParams: collateral_token: address minimum_debt: uint256 minimum_collateral_ratio: uint256 + max_penalty_collateral_ratio: uint256 + min_liquidation_fee: uint256 + max_liquidation_fee: uint256 upfront_interest_period: uint256 interest_rate_adj_cooldown: uint256 @@ -150,6 +154,7 @@ struct InitializeParams: _MAX_CALLBACK_DATA_SIZE: constant(uint256) = 10**5 _PRICE_ORACLE_PRECISION: constant(uint256) = 10 ** 36 +_LIQUIDATION_FEE_PRECISION: constant(uint256) = 10000 _WAD: constant(uint256) = 10 ** 18 _MAX_REDEMPTIONS: constant(uint256) = 1000 _ONE_YEAR: constant(uint256) = 365 * 60 * 60 * 24 @@ -175,6 +180,9 @@ one_pct: public(uint256) borrow_token_precision: public(uint256) min_debt: public(uint256) minimum_collateral_ratio: public(uint256) +max_penalty_collateral_ratio: public(uint256) +min_liquidation_fee: public(uint256) +max_liquidation_fee: public(uint256) upfront_interest_period: public(uint256) interest_rate_adj_cooldown: public(uint256) min_annual_interest_rate: public(uint256) @@ -216,14 +224,18 @@ def initialize(params: InitializeParams): # Get borrow token precision borrow_token_precision: uint256 = 10 ** convert(staticcall IERC20Detailed(params.borrow_token).decimals(), uint256) - # Define 1% using borrow token precision + # Define 1% and 0.01% using borrow token precision one_pct: uint256 = borrow_token_precision // 100 + one_hundredth_pct: uint256 = one_pct // 100 # Set market parameters self.one_pct = one_pct self.borrow_token_precision = borrow_token_precision self.min_debt = params.minimum_debt * borrow_token_precision self.minimum_collateral_ratio = params.minimum_collateral_ratio * one_pct + self.max_penalty_collateral_ratio = params.max_penalty_collateral_ratio * one_pct + self.min_liquidation_fee = params.min_liquidation_fee * one_hundredth_pct + self.max_liquidation_fee = params.max_liquidation_fee * one_hundredth_pct self.upfront_interest_period = params.upfront_interest_period self.interest_rate_adj_cooldown = params.interest_rate_adj_cooldown self.min_annual_interest_rate = one_pct // 2 # 0.5% @@ -945,18 +957,24 @@ def close_zombie_trove(trove_id: uint256): @external def liquidate_trove( trove_id: uint256, - max_amount: uint256 = max_value(uint256), + max_debt_to_decrease: uint256 = max_value(uint256), receiver: address = msg.sender, data: Bytes[_MAX_CALLBACK_DATA_SIZE] = empty(Bytes[_MAX_CALLBACK_DATA_SIZE]) ) -> uint256: """ - @notice Liquidate a single unhealthy Trove - @dev Uses the Dutch Desk contract to auction off the collateral tokens + @notice Liquidate a single unhealthy Trove (fully or partially) + @dev The liquidator repays debt and receives the equivalent collateral plus a dynamic bonus + that scales with the Trove's collateral ratio. If remaining debt would fall below + `min_debt`, the entire debt is liquidated. Full liquidations close the Trove and + return any excess collateral to the owner. Partial liquidations require the Trove + to end up above the minimum collateral ratio. Collateral is sent to the `receiver` + first, then, if `data` is non-empty, a `takeCallback` is then invoked on the `receiver`, + before debt is pulled @param trove_id Unique identifier of the unhealthy Trove - @param max_amount The maximum amount of debt to liquidate. Defaults to max uint256 - @param receiver The address that will receive the collateral tokens being sold. Defaults to msg.sender + @param max_debt_to_decrease The maximum amount of debt to liquidate. Defaults to max uint256 + @param receiver The address that will receive the collateral tokens. Defaults to msg.sender @param data The data to pass to the `receiver` callback. Defaults to empty - @return collateral_taken The amount of liquidated collateral tokens + @return The amount of collateral tokens sent to the `receiver` """ # Make sure the trove ID is non-zero assert trove_id != 0, "!trove_id" @@ -981,40 +999,103 @@ def liquidate_trove( trove.collateral, trove_debt_after_interest, collateral_price ) + # Cache the minimum collateral ratio + minimum_collateral_ratio: uint256 = self.minimum_collateral_ratio + # Make sure the collateral ratio is below the minimum collateral ratio - assert collateral_ratio < self.minimum_collateral_ratio, "!collateral_ratio" + assert collateral_ratio < minimum_collateral_ratio, "!collateral_ratio" + + # Determine the debt amount to decrease: + # - Cap at `max_debt_to_decrease` + # - If remaining debt would fall below `min_debt`, decrease the entire debt + debt_to_decrease: uint256 = min(trove_debt_after_interest, max_debt_to_decrease) + if trove_debt_after_interest - debt_to_decrease < self.min_debt: + debt_to_decrease = trove_debt_after_interest + + # Calculate the collateral to send to the `receiver`, capped at the Trove's collateral + collateral_to_decrease: uint256 = min( + self._calculate_collateral_to_decrease(collateral_ratio, debt_to_decrease, collateral_price), + trove.collateral, + ) - # Cache the Trove's info before changing it + # Cache the trove owner before the trove is potentially emptied trove_owner: address = trove.owner - collateral_to_decrease: uint256 = trove.collateral - debt_to_decrease: uint256 = trove_debt_after_interest - weighted_debt_to_decrease: uint256 = trove.debt * trove.annual_interest_rate - # Delete all Trove info and mark it as liquidated - trove = empty(Trove) - trove.status = Status.LIQUIDATED + # Check if this is a full liquidation and cache the result + is_full_liquidation: bool = debt_to_decrease == trove_debt_after_interest - # Save changes to storage - self.troves[trove_id] = trove + # Full liquidation: close the Trove and transfer any remaining collateral to the owner. + # Partial liquidation: reduce the Trove's debt and collateral and keep it open. + # After a partial liquidation, the Trove must end up above the minimum collateral ratio + if is_full_liquidation: + # Cache the Trove's collateral and weighted debt for global accounting + trove_collateral: uint256 = trove.collateral + trove_weighted_debt: uint256 = trove.debt * trove.annual_interest_rate - # If Trove is the current zombie trove, reset the `zombie_trove_id` variable - if self.zombie_trove_id == trove_id: - self.zombie_trove_id = 0 + # Delete all Trove info and mark it as liquidated + trove = empty(Trove) + trove.status = Status.LIQUIDATED - # Update the contract's recorded collateral balance - self.collateral_balance -= collateral_to_decrease + # Save changes to storage + self.troves[trove_id] = trove - # Accrue interest on the total debt and update accounting - self._accrue_interest_and_account_for_trove_change( - 0, # debt_increase - debt_to_decrease, # debt_decrease - 0, # weighted_debt_increase - weighted_debt_to_decrease, # weighted_debt_decrease - ) + # If Trove is the current zombie trove, reset the `zombie_trove_id` variable + if self.zombie_trove_id == trove_id: + self.zombie_trove_id = 0 + + # Update the contract's recorded collateral balance + self.collateral_balance -= trove_collateral + + # Accrue interest on the total debt and update accounting + self._accrue_interest_and_account_for_trove_change( + 0, # debt_increase + debt_to_decrease, # debt_decrease + 0, # weighted_debt_increase + trove_weighted_debt, # weighted_debt_decrease + ) + + # If Trove was active, remove from sorted list + if is_active: + extcall self.sorted_troves.remove(trove_id) + + # Calculate the remaining collateral to return to the trove owner (if any) + remaining_collateral: uint256 = trove_collateral - collateral_to_decrease + + # If needed, send the remaining collateral tokens to trove owner + if remaining_collateral > 0: + assert extcall self.collateral_token.transfer(trove_owner, remaining_collateral, default_return_value=True) + else: + # Calculate the new debt amount + new_debt: uint256 = trove_debt_after_interest - debt_to_decrease + + # Cache the Trove's old debt for global accounting + old_debt: uint256 = trove.debt + + # Calculate the new collateral amount and collateral ratio + new_collateral: uint256 = trove.collateral - collateral_to_decrease + new_collateral_ratio: uint256 = self._calculate_collateral_ratio(new_collateral, new_debt, collateral_price) + + # Make sure the new collateral ratio is above the minimum collateral ratio + assert new_collateral_ratio >= minimum_collateral_ratio, "!minimum_collateral_ratio" + + # Update the Trove's info + trove.debt = new_debt + trove.collateral = new_collateral + trove.last_debt_update_time = convert(block.timestamp, uint64) - # Remove from sorted list if Trove was active - if is_active: - extcall self.sorted_troves.remove(trove_id) + # Save changes to storage + self.troves[trove_id] = trove + + # Update the contract's recorded collateral balance + self.collateral_balance -= collateral_to_decrease + + # Accrue interest on the total debt and update accounting + self._accrue_interest_and_account_for_trove_change( + 0, # debt_increase + debt_to_decrease, # debt_decrease + new_debt * trove.annual_interest_rate, # weighted_debt_increase + old_debt * trove.annual_interest_rate, # weighted_debt_decrease + ) # Send the collateral tokens to the `receiver` assert extcall self.collateral_token.transfer(receiver, collateral_to_decrease, default_return_value=True) @@ -1039,6 +1120,7 @@ def liquidate_trove( liquidator=msg.sender, collateral_amount=collateral_to_decrease, debt_amount=debt_to_decrease, + is_full_liquidation=is_full_liquidation, ) # Return the amount of liquidated collateral tokens @@ -1270,6 +1352,52 @@ def _calculate_accrued_interest(weighted_debt: uint256, period: uint256) -> uint return weighted_debt * period // _ONE_YEAR // self.borrow_token_precision +@internal +@view +def _calculate_collateral_to_decrease( + collateral_ratio: uint256, + debt_to_decrease: uint256, + collateral_price: uint256, +) -> uint256: + """ + @notice Calculate the amount of collateral to decrease from a Trove on a liquidation + @dev The liquidator receives the debt-equivalent collateral plus a dynamic bonus. + The bonus percentage scales linearly from `min_liquidation_fee` (at minimum collateral ratio) to + `max_liquidation_fee` (at `max_penalty_collateral_ratio`), incentivizing + earlier liquidation of unhealthy troves. + Fee percentages are expressed in borrow token precision (same scale as collateral ratios) + @param collateral_ratio The Trove's current collateral ratio + @param debt_to_decrease The amount of debt being liquidated + @param collateral_price The current collateral price from the oracle + @return The amount of collateral tokens to decrease from the Trove (base + bonus) + """ + # Determine the liquidation fee percentage based on the collateral ratio: + # - At or above minimum collateral ratio --> minimum fee + # - At or below maximum penalty collateral ratio --> maximum fee + # - Between the two --> linear interpolation + liquidation_fee_pct: uint256 = 0 + minimum_collateral_ratio: uint256 = self.minimum_collateral_ratio + if collateral_ratio >= minimum_collateral_ratio: + liquidation_fee_pct = self.min_liquidation_fee + elif collateral_ratio <= self.max_penalty_collateral_ratio: + liquidation_fee_pct = self.max_liquidation_fee + else: + min_liquidation_fee: uint256 = self.min_liquidation_fee + collateral_ratio_range: uint256 = minimum_collateral_ratio - self.max_penalty_collateral_ratio + collateral_ratio_drop: uint256 = minimum_collateral_ratio - collateral_ratio + fee_range: uint256 = self.max_liquidation_fee - min_liquidation_fee + liquidation_fee_pct = min_liquidation_fee + (fee_range * collateral_ratio_drop // collateral_ratio_range) + + # Cache borrow token precision, as `liquidation_fee_pct` is in the same scale + liquidation_fee_precision: uint256 = self.borrow_token_precision + + # Convert the debt to its equivalent in collateral tokens + base_collateral: uint256 = debt_to_decrease * _PRICE_ORACLE_PRECISION // collateral_price + + # Apply the liquidation bonus + return base_collateral * (liquidation_fee_precision + liquidation_fee_pct) // liquidation_fee_precision + + @internal @view def _get_upfront_fee( diff --git a/test/Base.sol b/test/Base.sol index 6cbe8fa..2fde85c 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -12,6 +12,8 @@ import {IPriceOracle} from "./interfaces/IPriceOracle.sol"; import {ISortedTroves} from "./interfaces/ISortedTroves.sol"; import {ITroveManager} from "./interfaces/ITroveManager.sol"; +import {LiquidatorMock} from "./mocks/LiquidatorMock.sol"; + import "../script/Deploy.s.sol"; import "forge-std/Test.sol"; @@ -25,6 +27,7 @@ abstract contract Base is Deploy, Test { IDutchDesk public dutchDesk; ISortedTroves public sortedTroves; ITroveManager public troveManager; + LiquidatorMock public liquidatorMock; // Roles address public userLender = address(420); @@ -38,6 +41,9 @@ abstract contract Base is Deploy, Test { // Market parameters uint256 public minimumDebt = 500; // 500 tokens uint256 public minimumCollateralRatio = 110; // 110% + uint256 public maxPenaltyCollateralRatio = 105; // 105% + uint256 public minLiquidationFee = 50; // 0.5% + uint256 public maxLiquidationFee = 500; // 5% uint256 public upfrontInterestPeriod = 7 days; // 7 days uint256 public interestRateAdjCooldown = 7 days; // 7 days uint256 public minimumPriceBufferPercentage = 1e18 - 5e16; // 95% @@ -56,7 +62,6 @@ abstract contract Base is Deploy, Test { uint256 public DEFAULT_ANNUAL_INTEREST_RATE; uint256 public DEFAULT_TARGET_COLLATERAL_RATIO; - uint256 public constant MAX_LIQUIDATIONS = 20; uint256 public constant ORACLE_PRICE_SCALE = 1e36; uint256 public constant WAD = 1e18; @@ -84,6 +89,9 @@ abstract contract Base is Deploy, Test { performance_fee_recipient: performanceFeeRecipient, minimum_debt: minimumDebt, minimum_collateral_ratio: minimumCollateralRatio, + max_penalty_collateral_ratio: maxPenaltyCollateralRatio, + min_liquidation_fee: minLiquidationFee, + max_liquidation_fee: maxLiquidationFee, upfront_interest_period: upfrontInterestPeriod, interest_rate_adj_cooldown: interestRateAdjCooldown, minimum_price_buffer_percentage: minimumPriceBufferPercentage, @@ -103,6 +111,9 @@ abstract contract Base is Deploy, Test { auction = IAuction(_auction); lender = ILender(_lender); + // Deploy liquidator mock + liquidatorMock = new LiquidatorMock(troveManager, borrowToken); + // Label addresses vm.label(address(troveManager), "TroveManager"); vm.label(address(sortedTroves), "SortedTroves"); @@ -154,6 +165,12 @@ abstract contract Base is Deploy, Test { } } + function liquidate( + uint256 _troveId + ) public returns (uint256) { + return liquidatorMock.liquidate(_troveId, type(uint256).max); + } + function takeAuction( uint256 _auctionId ) public returns (uint256) { @@ -207,6 +224,35 @@ abstract contract Base is Deploy, Test { depositIntoLender(_user, _amount); } + function calculateCollateralToDecrease( + uint256 _collateralRatio, + uint256 _debtToDecrease, + uint256 _collateralPrice, + uint256 _troveCollateral + ) public view returns (uint256) { + uint256 _liquidationFeePct; + uint256 _mcr = troveManager.minimum_collateral_ratio(); + uint256 _maxPenaltyCR = troveManager.max_penalty_collateral_ratio(); + + if (_collateralRatio >= _mcr) { + _liquidationFeePct = troveManager.min_liquidation_fee(); + } else if (_collateralRatio <= _maxPenaltyCR) { + _liquidationFeePct = troveManager.max_liquidation_fee(); + } else { + uint256 _minFee = troveManager.min_liquidation_fee(); + uint256 _feeRange = troveManager.max_liquidation_fee() - _minFee; + uint256 _crDrop = _mcr - _collateralRatio; + uint256 _crRange = _mcr - _maxPenaltyCR; + _liquidationFeePct = _minFee + (_feeRange * _crDrop / _crRange); + } + + uint256 _baseCollateral = _debtToDecrease * ORACLE_PRICE_SCALE / _collateralPrice; + uint256 _collateralToDecrease = _baseCollateral * (BORROW_TOKEN_PRECISION + _liquidationFeePct) / BORROW_TOKEN_PRECISION; + + if (_collateralToDecrease > _troveCollateral) return _troveCollateral; + return _collateralToDecrease; + } + function mintAndOpenTrove( address _user, uint256 _collateralAmount, diff --git a/test/Gas.t.sol b/test/Gas.t.sol index 7e5ae4b..e50bf17 100644 --- a/test/Gas.t.sol +++ b/test/Gas.t.sol @@ -89,44 +89,4 @@ contract GasTests is Base { assertLt(gasUsed, MAX_GAS, "Exceeded 7M gas limit"); } - // @todo - // function test_gas_liquidateTroves() public { - // uint256 _minDebt = troveManager.min_debt(); - // uint256 _rate = DEFAULT_ANNUAL_INTEREST_RATE; - // uint256 _numTroves = MAX_LIQUIDATIONS; - - // uint256 _lenderDeposit = _minDebt * _numTroves; - - // // Fund lender - // mintAndDepositIntoLender(userLender, _lenderDeposit); - - // // Create troves and store their IDs - // uint256[MAX_LIQUIDATIONS] memory _troveIds; - // for (uint256 i = 0; i < _numTroves; i++) { - // uint256 _collateralNeeded = - // (_minDebt * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - - // address _user = address(uint160(i + 1000)); - // _troveIds[i] = mintAndOpenTrove(_user, _collateralNeeded, _minDebt, _rate); - // } - - // // Drop price to make all troves liquidatable - // uint256 _priceDropToBelowMCR = priceOracle.get_price() * 80 / 100; // 20% drop - // uint256 _priceDropToBelowMCR18 = priceOracle.get_price(false) * 80 / 100; // 20% drop - // vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); - // vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - - // // Liquidate all troves - // uint256 _gasBefore = gasleft(); - // troveManager.liquidate_troves(_troveIds); - // uint256 _gasUsed = _gasBefore - gasleft(); - - // emit log_named_uint("Troves liquidated", _numTroves); - // emit log_named_uint("Gas used", _gasUsed); - // emit log_named_uint("Cost in ETH (wei)", _gasUsed * GAS_PRICE); - - // assertLt(_gasUsed, MAX_GAS + 1_000_000, "Exceeded 8M gas limit"); - // } - - } diff --git a/test/Liquidate.t.sol b/test/Liquidate.t.sol index a3a5bc0..9543a9c 100644 --- a/test/Liquidate.t.sol +++ b/test/Liquidate.t.sol @@ -1,666 +1,775 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity 0.8.23; - -// import "./Base.sol"; -// import {IPriceOracleNotScaled} from "./interfaces/IPriceOracleNotScaled.sol"; -// import {IPriceOracleScaled} from "./interfaces/IPriceOracleScaled.sol"; - -// contract LiquidateTests is Base { - -// function setUp() public override { -// Base.setUp(); - -// // Set `profitMaxUnlockTime` to 0 -// vm.prank(management); -// lender.setProfitMaxUnlockTime(0); - -// // Set fees to 0 -// vm.prank(management); -// lender.setPerformanceFee(0); -// } - -// // 1. lend -// // 2. borrow all available liquidity -// // 3. collateral price drops -// // 4. liquidate trove -// function test_liquidateTrove( -// uint256 _amount -// ) public { -// _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); - -// // Lend some from lender -// mintAndDepositIntoLender(userLender, _amount); - -// // Calculate how much collateral is needed for the borrow amount -// uint256 _collateralNeeded = -// (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - -// // Calculate expected debt (borrow amount + upfront fee) -// uint256 _expectedDebt = _amount + troveManager.get_upfront_fee(_amount, DEFAULT_ANNUAL_INTEREST_RATE); - -// // Open a trove -// uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); - -// // Check trove info -// ITroveManager.Trove memory _trove = troveManager.troves(_troveId); -// assertEq(_trove.debt, _expectedDebt, "E0"); -// assertEq(_trove.collateral, _collateralNeeded, "E1"); -// assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E2"); -// assertEq(_trove.last_debt_update_time, block.timestamp, "E3"); -// assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E4"); -// assertEq(_trove.owner, userBorrower, "E5"); -// assertEq(_trove.pending_owner, address(0), "E6"); -// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E7"); -// assertApproxEqRel( -// (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, -// DEFAULT_TARGET_COLLATERAL_RATIO, -// 1e15, -// "E8" -// ); // 0.1% - -// // Check sorted troves -// assertFalse(sortedTroves.empty(), "E9"); -// assertEq(sortedTroves.size(), 1, "E10"); -// assertEq(sortedTroves.first(), _troveId, "E11"); -// assertEq(sortedTroves.last(), _troveId, "E12"); -// assertTrue(sortedTroves.contains(_troveId), "E13"); - -// // Check balances -// assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded, "E14"); -// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E15"); -// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E16"); -// assertEq(borrowToken.balanceOf(address(lender)), 0, "E17"); -// assertEq(borrowToken.balanceOf(userBorrower), _amount, "E18"); - -// // Check global info -// assertEq(troveManager.total_debt(), _expectedDebt, "E19"); -// assertEq(troveManager.total_weighted_debt(), _expectedDebt * DEFAULT_ANNUAL_INTEREST_RATE, "E20"); -// assertEq(troveManager.collateral_balance(), _collateralNeeded, "E21"); -// assertEq(troveManager.zombie_trove_id(), 0, "E22"); - -// // Check dutch desk is empty -// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E23"); -// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E24"); - -// // CR = (collateral * price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / debt -// // So price_at_MCR = MCR * debt * ORACLE_PRICE_SCALE / (collateral * BORROW_TOKEN_PRECISION) -// // We want to be 1% below MCR -// uint256 _priceDropToBelowMCR; -// if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { -// // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow -// _priceDropToBelowMCR = -// troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); -// } else { -// // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow -// _priceDropToBelowMCR = -// troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; -// } -// uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); - -// // Drop collateral price to put trove below MCR -// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); -// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - -// // Make sure price actually dropped -// assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E25"); - -// // Calculate Trove's collateral ratio after price drop -// uint256 _troveCollateralRatioAfter = (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; - -// // Make sure Trove is below MCR -// assertLt(_troveCollateralRatioAfter, troveManager.minimum_collateral_ratio(), "E26"); - -// // Finally, liquidate the trove -// { -// vm.startPrank(liquidator); -// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; -// _troveIdsToLiquidate[0] = _troveId; -// troveManager.liquidate_troves(_troveIdsToLiquidate); -// vm.stopPrank(); -// } - -// // Check liquidator received no fee (all collateral goes to auction) -// assertEq(collateralToken.balanceOf(liquidator), 0, "E26a"); - -// // Check auction starting price and minimum price -// { -// uint256 _expectedStartingPrice = _collateralNeeded * _priceDropToBelowMCR18 * dutchDesk.liquidation_starting_price_buffer_percentage() -// / 1e18 / COLLATERAL_TOKEN_PRECISION; -// assertEq(auction.auctions(0).startingPrice, _expectedStartingPrice, "E27"); -// uint256 _expectedMinimumPrice = _priceDropToBelowMCR18 * dutchDesk.liquidation_minimum_price_buffer_percentage() / WAD; -// assertEq(auction.auctions(0).minimumPrice, _expectedMinimumPrice, "E28"); -// } - -// // Take the auction -// takeAuction(0); - -// // Make sure lender got all the borrow tokens back + liquidation fee -// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E29"); - -// // Make sure liquidator got the collateral -// assertEq(collateralToken.balanceOf(liquidator), _collateralNeeded, "E30"); - -// // Check everything again - -// // Check trove info -// _trove = troveManager.troves(_troveId); -// assertEq(_trove.debt, 0, "E31"); -// assertEq(_trove.collateral, 0, "E32"); -// assertEq(_trove.annual_interest_rate, 0, "E33"); -// assertEq(_trove.last_debt_update_time, 0, "E34"); -// assertEq(_trove.last_interest_rate_adj_time, 0, "E35"); -// assertEq(_trove.owner, address(0), "E36"); -// assertEq(_trove.pending_owner, address(0), "E37"); -// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E38"); - -// // Check sorted troves -// assertTrue(sortedTroves.empty(), "E39"); -// assertEq(sortedTroves.size(), 0, "E40"); -// assertEq(sortedTroves.first(), 0, "E41"); -// assertEq(sortedTroves.last(), 0, "E42"); -// assertFalse(sortedTroves.contains(_troveId), "E43"); - -// // Check balances -// assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E44"); -// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E45"); -// assertEq(collateralToken.balanceOf(address(userBorrower)), 0, "E46"); -// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E47"); -// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E48"); -// assertEq(borrowToken.balanceOf(userBorrower), _amount, "E49"); - -// // Check global info -// assertEq(troveManager.total_debt(), 0, "E50"); -// assertEq(troveManager.total_weighted_debt(), 0, "E51"); -// assertEq(troveManager.collateral_balance(), 0, "E52"); -// assertEq(troveManager.zombie_trove_id(), 0, "E53"); - -// // Check dutch desk is empty -// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E54"); -// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E55"); -// } - -// // 1. lend -// // 2. borrow half of available liquidity from 1st borrower -// // 3. borrow half of available liquidity from 2nd borrower -// // 4. collateral price drops -// // 5. liquidate both troves -// function test_liquidateTroves( -// uint256 _amount -// ) public { -// _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); - -// // Lend some from lender -// mintAndDepositIntoLender(userLender, _amount); - -// uint256 _halfAmount = _amount / 2; - -// // Calculate how much collateral is needed for the borrow amount -// uint256 _collateralNeeded = -// (_halfAmount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - -// // Calculate expected debt (borrow amount + upfront fee) -// uint256 _expectedDebt = _halfAmount + troveManager.get_upfront_fee(_halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - -// // Open a trove for the first borrower -// uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - -// // Check trove info -// ITroveManager.Trove memory _trove = troveManager.troves(_troveId); -// assertEq(_trove.debt, _expectedDebt, "E0"); -// assertEq(_trove.collateral, _collateralNeeded, "E1"); -// assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E2"); -// assertEq(_trove.last_debt_update_time, block.timestamp, "E3"); -// assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E4"); -// assertEq(_trove.owner, userBorrower, "E5"); -// assertEq(_trove.pending_owner, address(0), "E6"); -// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E7"); -// assertApproxEqRel( -// (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, -// DEFAULT_TARGET_COLLATERAL_RATIO, -// 1e15, -// "E8" -// ); // 0.1% - -// // Check sorted troves -// assertFalse(sortedTroves.empty(), "E9"); -// assertEq(sortedTroves.size(), 1, "E10"); -// assertEq(sortedTroves.first(), _troveId, "E11"); -// assertEq(sortedTroves.last(), _troveId, "E12"); -// assertTrue(sortedTroves.contains(_troveId), "E13"); - -// // Check balances -// assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded, "E14"); -// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E15"); -// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E16"); -// assertApproxEqAbs(borrowToken.balanceOf(address(lender)), _halfAmount, 1, "E17"); -// assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E18"); - -// // Check global info -// assertEq(troveManager.total_debt(), _expectedDebt, "E19"); -// assertEq(troveManager.total_weighted_debt(), _expectedDebt * DEFAULT_ANNUAL_INTEREST_RATE, "E20"); -// assertEq(troveManager.collateral_balance(), _collateralNeeded, "E21"); -// assertEq(troveManager.zombie_trove_id(), 0, "E22"); - -// // Check dutch desk is empty -// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E23"); -// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E24"); - -// // Open a trove for the second borrower -// uint256 _anotherTroveId = mintAndOpenTrove(anotherUserBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - -// // Check trove info -// _trove = troveManager.troves(_anotherTroveId); -// assertEq(_trove.debt, _expectedDebt, "E25"); -// assertEq(_trove.collateral, _collateralNeeded, "E26"); -// assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E27"); -// assertEq(_trove.last_debt_update_time, block.timestamp, "E28"); -// assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E29"); -// assertEq(_trove.owner, anotherUserBorrower, "E30"); -// assertEq(_trove.pending_owner, address(0), "E31"); -// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E32"); -// assertApproxEqRel( -// (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, -// DEFAULT_TARGET_COLLATERAL_RATIO, -// 1e15, -// "E33" -// ); // 0.1% - -// // Check sorted troves -// assertFalse(sortedTroves.empty(), "E34"); -// assertEq(sortedTroves.size(), 2, "E35"); -// assertEq(sortedTroves.first(), _troveId, "E36"); -// assertEq(sortedTroves.last(), _anotherTroveId, "E37"); -// assertTrue(sortedTroves.contains(_troveId), "E38"); -// assertTrue(sortedTroves.contains(_anotherTroveId), "E39"); - -// // Check balances -// assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded * 2, "E40"); -// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E41"); -// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E42"); -// assertApproxEqAbs(borrowToken.balanceOf(address(lender)), 0, 1, "E43"); -// assertEq(borrowToken.balanceOf(anotherUserBorrower), _halfAmount, "E44"); - -// // Check global info -// assertEq(troveManager.total_debt(), _expectedDebt * 2, "E45"); -// assertEq(troveManager.total_weighted_debt(), _expectedDebt * 2 * DEFAULT_ANNUAL_INTEREST_RATE, "E46"); -// assertEq(troveManager.collateral_balance(), _collateralNeeded * 2, "E47"); -// assertEq(troveManager.zombie_trove_id(), 0, "E48"); - -// // Check dutch desk is empty -// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E49"); -// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E50"); - -// // CR = (collateral * price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / debt -// // So price_at_MCR = MCR * debt * ORACLE_PRICE_SCALE / (collateral * BORROW_TOKEN_PRECISION) -// // We want to be 1% below MCR -// uint256 _priceDropToBelowMCR; -// if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { -// // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow -// _priceDropToBelowMCR = -// troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); -// } else { -// // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow -// _priceDropToBelowMCR = -// troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; -// } -// uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); - -// // Drop collateral price to put trove below MCR -// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); -// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - -// // Make sure price actually dropped -// assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E51"); - -// // Calculate Trove's collateral ratio after price drop -// uint256 _troveCollateralRatioAfter = (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; - -// // Make sure Trove is below MCR -// assertLt(_troveCollateralRatioAfter, troveManager.minimum_collateral_ratio(), "E52"); - -// // Finally, liquidate the troves -// { -// vm.startPrank(liquidator); -// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; -// _troveIdsToLiquidate[0] = _troveId; -// _troveIdsToLiquidate[1] = _anotherTroveId; -// troveManager.liquidate_troves(_troveIdsToLiquidate); -// vm.stopPrank(); -// } - -// // Check liquidator received no fee (all collateral goes to auction) -// assertEq(collateralToken.balanceOf(liquidator), 0, "E53"); - -// // Check auction starting price and minimum price -// // Starting price = available * price * STARTING_PRICE_BUFFER_PERCENTAGE / 1e18 / COLLATERAL_TOKEN_PRECISION -// // Note: both troves liquidated in same tx, so all collateral goes to auction -// { -// uint256 _collateralToSell = _collateralNeeded * 2; -// uint256 _expectedStartingPrice = _collateralToSell * _priceDropToBelowMCR18 * dutchDesk.liquidation_starting_price_buffer_percentage() -// / 1e18 / COLLATERAL_TOKEN_PRECISION; -// assertEq(auction.auctions(0).startingPrice, _expectedStartingPrice, "E54"); -// uint256 _expectedMinimumPrice = _priceDropToBelowMCR18 * dutchDesk.liquidation_minimum_price_buffer_percentage() / WAD; -// assertEq(auction.auctions(0).minimumPrice, _expectedMinimumPrice, "E55"); -// } - -// // Take the auction -// takeAuction(0); - -// // Make sure lender got all the borrow tokens back + liquidation fee -// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt * 2, "E56"); - -// // Make sure liquidator got the collateral -// assertEq(collateralToken.balanceOf(liquidator), _collateralNeeded * 2, "E57"); - -// // Check everything again - -// // Check trove info -// _trove = troveManager.troves(_troveId); -// assertEq(_trove.debt, 0, "E58"); -// assertEq(_trove.collateral, 0, "E59"); -// assertEq(_trove.annual_interest_rate, 0, "E60"); -// assertEq(_trove.last_debt_update_time, 0, "E61"); -// assertEq(_trove.last_interest_rate_adj_time, 0, "E62"); -// assertEq(_trove.owner, address(0), "E63"); -// assertEq(_trove.pending_owner, address(0), "E64"); -// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E65"); - -// // Check sorted troves -// assertTrue(sortedTroves.empty(), "E66"); -// assertEq(sortedTroves.size(), 0, "E67"); -// assertEq(sortedTroves.first(), 0, "E68"); -// assertEq(sortedTroves.last(), 0, "E69"); -// assertFalse(sortedTroves.contains(_troveId), "E70"); - -// // Check balances -// assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E71"); -// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E72"); -// assertEq(collateralToken.balanceOf(address(userBorrower)), 0, "E73"); -// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E74"); -// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E75"); -// assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E76"); - -// // Check global info -// assertEq(troveManager.total_debt(), 0, "E77"); -// assertEq(troveManager.total_weighted_debt(), 0, "E78"); -// assertEq(troveManager.collateral_balance(), 0, "E79"); -// assertEq(troveManager.zombie_trove_id(), 0, "E80"); - -// // Check dutch desk is empty -// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E81"); -// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E82"); - -// // Check everything again for the second trove - -// // Check trove info -// _trove = troveManager.troves(_anotherTroveId); -// assertEq(_trove.debt, 0, "E83"); -// assertEq(_trove.collateral, 0, "E84"); -// assertEq(_trove.annual_interest_rate, 0, "E85"); -// assertEq(_trove.last_debt_update_time, 0, "E86"); -// assertEq(_trove.last_interest_rate_adj_time, 0, "E87"); -// assertEq(_trove.owner, address(0), "E88"); -// assertEq(_trove.pending_owner, address(0), "E89"); -// assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E90"); - -// // Check sorted troves -// assertTrue(sortedTroves.empty(), "E91"); -// assertEq(sortedTroves.size(), 0, "E92"); -// assertEq(sortedTroves.first(), 0, "E93"); -// assertEq(sortedTroves.last(), 0, "E94"); -// assertFalse(sortedTroves.contains(_anotherTroveId), "E95"); - -// // Check balances -// assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E96"); -// assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E97"); -// assertEq(collateralToken.balanceOf(address(userBorrower)), 0, "E98"); -// assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E99"); -// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E100"); -// assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E101"); - -// // Check global info -// assertEq(troveManager.total_debt(), 0, "E102"); -// assertEq(troveManager.total_weighted_debt(), 0, "E103"); -// assertEq(troveManager.collateral_balance(), 0, "E104"); -// assertEq(troveManager.zombie_trove_id(), 0, "E105"); - -// // Check dutch desk is empty -// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E106"); -// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E107"); -// } - -// // 1. lend -// // 2. open 2 troves -// // 3. collateral price drops -// // 4. liquidate first trove -// // 5. liquidate second trove (before taking the first auction) -// // 6. take the auction (should have collateral from both troves - DutchDesk sweeps + settles + re-kicks with all collateral) -// function test_liquidateTroves_sequentialLiquidations( -// uint256 _amount -// ) public { -// _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); - -// // Lend some from lender -// mintAndDepositIntoLender(userLender, _amount); - -// uint256 _halfAmount = _amount / 2; - -// // Calculate how much collateral is needed for the borrow amount -// uint256 _collateralNeeded = -// (_halfAmount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - -// // Calculate expected debt (borrow amount + upfront fee) -// uint256 _expectedDebt = _halfAmount + troveManager.get_upfront_fee(_halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - -// // Open first trove -// uint256 _troveId1 = mintAndOpenTrove(userBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - -// // Open second trove -// uint256 _troveId2 = mintAndOpenTrove(anotherUserBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); - -// // Check trove info for first trove -// ITroveManager.Trove memory _trove1 = troveManager.troves(_troveId1); -// assertEq(_trove1.debt, _expectedDebt, "E0"); -// assertEq(_trove1.collateral, _collateralNeeded, "E1"); - -// // Check trove info for second trove -// ITroveManager.Trove memory _trove2 = troveManager.troves(_troveId2); -// assertEq(_trove2.debt, _expectedDebt, "E2"); -// assertEq(_trove2.collateral, _collateralNeeded, "E3"); - -// // Check dutch desk is empty -// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E4"); -// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E5"); - -// // CR = collateral * price / debt, so price_at_MCR = MCR * debt / collateral -// // We want to be 1% below MCR -// uint256 _priceDropToBelowMCR; -// if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { -// // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow -// _priceDropToBelowMCR = troveManager.minimum_collateral_ratio() * _trove1.debt * ORACLE_PRICE_SCALE * 99 -// / (100 * _trove1.collateral * BORROW_TOKEN_PRECISION); -// } else { -// // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow -// _priceDropToBelowMCR = -// troveManager.minimum_collateral_ratio() * _trove1.debt * 99 / 100 / _trove1.collateral * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION; -// } -// uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); - -// // Drop collateral price to put both troves below MCR -// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); -// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - -// // Make sure price actually dropped -// assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E6"); - -// // Make sure both troves are below MCR -// assertLt( -// (_trove1.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove1.debt, -// troveManager.minimum_collateral_ratio(), -// "E7" -// ); -// assertLt( -// (_trove2.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove2.debt, -// troveManager.minimum_collateral_ratio(), -// "E8" -// ); - -// // Liquidate the first trove -// vm.startPrank(liquidator); -// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; -// _troveIdsToLiquidate[0] = _troveId1; -// troveManager.liquidate_troves(_troveIdsToLiquidate); -// vm.stopPrank(); - -// // Check liquidator received no fee (all collateral goes to auction) -// assertEq(collateralToken.balanceOf(liquidator), 0, "E9"); - -// // Check auction 0 has all collateral from first trove -// // uint256 _auctionId0 = 0; -// { -// uint256 _collateralInAuction = _collateralNeeded; -// assertTrue(auction.is_active(0), "E10"); -// assertEq(auction.get_available_amount(0), _collateralInAuction, "E11"); -// assertEq(collateralToken.balanceOf(address(auction)), _collateralInAuction, "E12"); - -// // Check auction starting price and minimum price after first liquidation -// assertEq( -// auction.auctions(0).startingPrice, -// _collateralInAuction * _priceDropToBelowMCR18 * dutchDesk.liquidation_starting_price_buffer_percentage() / 1e18 -// / COLLATERAL_TOKEN_PRECISION, -// "E13" -// ); -// assertEq(auction.auctions(0).minimumPrice, _priceDropToBelowMCR18 * dutchDesk.liquidation_minimum_price_buffer_percentage() / WAD, "E14"); -// } - -// // Check first trove is liquidated -// _trove1 = troveManager.troves(_troveId1); -// assertEq(uint256(troveManager.troves(_troveId1).status), uint256(ITroveManager.Status.liquidated), "E15"); - -// // Check second trove is still active -// _trove2 = troveManager.troves(_troveId2); -// assertEq(uint256(_trove2.status), uint256(ITroveManager.Status.active), "E16"); - -// // Liquidate the second trove (before taking the first auction) -// // In new architecture, this creates a separate auction with ID 1 -// vm.startPrank(liquidator); -// _troveIdsToLiquidate[0] = _troveId2; -// troveManager.liquidate_troves(_troveIdsToLiquidate); -// vm.stopPrank(); - -// // Check liquidator received no fees (all collateral goes to auction) -// assertEq(collateralToken.balanceOf(liquidator), 0, "E17"); - -// // Check second trove is now liquidated -// _trove2 = troveManager.troves(_troveId2); -// assertEq(uint256(_trove2.status), uint256(ITroveManager.Status.liquidated), "E18"); - -// // Check auction 1 has all collateral from second trove (separate auction) -// // uint256 _auctionId1 = 1; -// { -// uint256 _collateralInAuction = _collateralNeeded; -// assertTrue(auction.is_active(1), "E19"); -// assertEq(auction.get_available_amount(1), _collateralInAuction, "E20"); -// assertEq(collateralToken.balanceOf(address(auction)), _collateralInAuction * 2, "E21"); - -// // Check auction 0 is still active with first trove's collateral -// assertTrue(auction.is_active(0), "E22"); -// assertEq(auction.get_available_amount(0), _collateralInAuction, "E23"); -// } - -// // Take both auctions -// takeAuction(0); -// takeAuction(1); - -// // Both auctions should be empty now -// assertEq(collateralToken.balanceOf(address(auction)), 0, "E24"); -// assertFalse(auction.is_active(0), "E25"); -// assertFalse(auction.is_active(1), "E26"); - -// // Make sure lender got all the borrow tokens back + liquidation fees -// assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt * 2, "E27"); - -// // Make sure liquidator got all the collateral (fees + auction proceeds) -// assertEq(collateralToken.balanceOf(liquidator), _collateralNeeded * 2, "E28"); - -// // Check dutch desk is empty -// assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E29"); -// assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E30"); - -// // Check global info -// assertEq(troveManager.total_debt(), 0, "E31"); -// assertEq(troveManager.total_weighted_debt(), 0, "E32"); -// assertEq(troveManager.collateral_balance(), 0, "E33"); -// } - -// function test_liquidateTroves_emptyList() public { -// // Make sure we always fail when no trove ids are passed -// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; -// vm.expectRevert("!trove_ids"); -// troveManager.liquidate_troves(_troveIdsToLiquidate); -// } - -// function test_liquidateTroves_nonExistentTrove( -// uint256 _nonExistentTroveId -// ) public { -// _nonExistentTroveId = bound(_nonExistentTroveId, 1, type(uint256).max); -// // Make sure we always fail when a non-existent trove is passed -// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; -// _troveIdsToLiquidate[0] = _nonExistentTroveId; -// vm.expectRevert("!active or zombie"); -// troveManager.liquidate_troves(_troveIdsToLiquidate); -// } - -// function test_liquidateTroves_aboveMCR( -// uint256 _amount -// ) public { -// _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); - -// // Lend some from lender -// mintAndDepositIntoLender(userLender, _amount); - -// // Calculate how much collateral is needed for the borrow amount -// uint256 _collateralNeeded = -// (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - -// // Open a trove -// uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); - -// // Make sure we cannot liquidate a trove that is above MCR -// uint256[MAX_LIQUIDATIONS] memory _troveIdsToLiquidate; -// _troveIdsToLiquidate[0] = _troveId; -// vm.expectRevert("!collateral_ratio"); -// troveManager.liquidate_troves(_troveIdsToLiquidate); -// } - -// function test_liquidateGas() public { -// uint256 _amount = troveManager.min_debt() * BORROW_TOKEN_PRECISION; - -// mintAndDepositIntoLender(userLender, _amount); - -// uint256 _collateralNeeded = -// (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); - -// uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); - -// // Drop price below MCR -// ITroveManager.Trove memory _trove = troveManager.troves(_troveId); -// uint256 _priceDropToBelowMCR = -// troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); -// uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); - -// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); -// vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); - -// uint256[MAX_LIQUIDATIONS] memory _troveIds; -// _troveIds[0] = _troveId; - -// uint256 _gasBefore = gasleft(); -// vm.prank(liquidator); -// troveManager.liquidate_troves(_troveIds); -// uint256 _gasUsed = _gasBefore - gasleft(); - -// console2.log("Gas used to liquidate 1 trove:", _gasUsed); -// } - -// } +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import "./Base.sol"; +import {IPriceOracleNotScaled} from "./interfaces/IPriceOracleNotScaled.sol"; +import {IPriceOracleScaled} from "./interfaces/IPriceOracleScaled.sol"; + +contract LiquidateTests is Base { + + function setUp() public override { + Base.setUp(); + + // Set `profitMaxUnlockTime` to 0 + vm.prank(management); + lender.setProfitMaxUnlockTime(0); + + // Set fees to 0 + vm.prank(management); + lender.setPerformanceFee(0); + } + + // 1. lend + // 2. borrow all available liquidity + // 3. collateral price drops + // 4. liquidate trove + function test_liquidateTrove( + uint256 _amount + ) public { + _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); + + // Lend some from lender + mintAndDepositIntoLender(userLender, _amount); + + // Calculate how much collateral is needed for the borrow amount + uint256 _collateralNeeded = + (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + // Calculate expected debt (borrow amount + upfront fee) + uint256 _expectedDebt = _amount + troveManager.get_upfront_fee(_amount, DEFAULT_ANNUAL_INTEREST_RATE); + + // Open a trove + uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + + // Check trove info + ITroveManager.Trove memory _trove = troveManager.troves(_troveId); + assertEq(_trove.debt, _expectedDebt, "E0"); + assertEq(_trove.collateral, _collateralNeeded, "E1"); + assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E2"); + assertEq(_trove.last_debt_update_time, block.timestamp, "E3"); + assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E4"); + assertEq(_trove.owner, userBorrower, "E5"); + assertEq(_trove.pending_owner, address(0), "E6"); + assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E7"); + assertApproxEqRel( + (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, + DEFAULT_TARGET_COLLATERAL_RATIO, + 1e15, + "E8" + ); // 0.1% + + // Check sorted troves + assertFalse(sortedTroves.empty(), "E9"); + assertEq(sortedTroves.size(), 1, "E10"); + assertEq(sortedTroves.first(), _troveId, "E11"); + assertEq(sortedTroves.last(), _troveId, "E12"); + assertTrue(sortedTroves.contains(_troveId), "E13"); + + // Check balances + assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded, "E14"); + assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E15"); + assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E16"); + assertEq(borrowToken.balanceOf(address(lender)), 0, "E17"); + assertEq(borrowToken.balanceOf(userBorrower), _amount, "E18"); + + // Check global info + assertEq(troveManager.total_debt(), _expectedDebt, "E19"); + assertEq(troveManager.total_weighted_debt(), _expectedDebt * DEFAULT_ANNUAL_INTEREST_RATE, "E20"); + assertEq(troveManager.collateral_balance(), _collateralNeeded, "E21"); + assertEq(troveManager.zombie_trove_id(), 0, "E22"); + + // Check dutch desk is empty + assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E23"); + assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E24"); + + // CR = (collateral * price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / debt + // So price_at_MCR = MCR * debt * ORACLE_PRICE_SCALE / (collateral * BORROW_TOKEN_PRECISION) + // We want to be 1% below MCR + uint256 _priceDropToBelowMCR; + if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { + // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow + _priceDropToBelowMCR = + troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); + } else { + // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow + _priceDropToBelowMCR = + troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; + } + uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); + + // Drop collateral price to put trove below MCR + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); + + // Make sure price actually dropped + assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E25"); + + // Calculate Trove's collateral ratio after price drop + uint256 _troveCollateralRatioAfter = (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; + + // Make sure Trove is below MCR + assertLt(_troveCollateralRatioAfter, troveManager.minimum_collateral_ratio(), "E26"); + + // Calculate expected collateral to decrease + uint256 _expectedCollateralToDecrease = + calculateCollateralToDecrease(_troveCollateralRatioAfter, _expectedDebt, _priceDropToBelowMCR, _collateralNeeded); + uint256 _expectedRemainingCollateral = _collateralNeeded - _expectedCollateralToDecrease; + + // Finally, liquidate the trove + liquidate(_troveId); + + // Make sure lender got all the borrow tokens back + assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E27"); + + // Make sure liquidator mock got the collateral + assertEq(collateralToken.balanceOf(address(liquidatorMock)), _expectedCollateralToDecrease, "E28"); + + // Check everything again + + // Check trove info + _trove = troveManager.troves(_troveId); + assertEq(_trove.debt, 0, "E29"); + assertEq(_trove.collateral, 0, "E30"); + assertEq(_trove.annual_interest_rate, 0, "E31"); + assertEq(_trove.last_debt_update_time, 0, "E32"); + assertEq(_trove.last_interest_rate_adj_time, 0, "E33"); + assertEq(_trove.owner, address(0), "E34"); + assertEq(_trove.pending_owner, address(0), "E35"); + assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E36"); + + // Check sorted troves + assertTrue(sortedTroves.empty(), "E37"); + assertEq(sortedTroves.size(), 0, "E38"); + assertEq(sortedTroves.first(), 0, "E39"); + assertEq(sortedTroves.last(), 0, "E40"); + assertFalse(sortedTroves.contains(_troveId), "E41"); + + // Check balances + assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E42"); + assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E43"); + assertEq(collateralToken.balanceOf(address(userBorrower)), _expectedRemainingCollateral, "E44"); + assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E45"); + assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E46"); + assertEq(borrowToken.balanceOf(userBorrower), _amount, "E47"); + + // Check global info + assertEq(troveManager.total_debt(), 0, "E48"); + assertEq(troveManager.total_weighted_debt(), 0, "E49"); + assertEq(troveManager.collateral_balance(), 0, "E50"); + assertEq(troveManager.zombie_trove_id(), 0, "E51"); + + // Check dutch desk is empty + assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E52"); + assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E53"); + } + + // 1. lend + // 2. borrow half of available liquidity from 1st borrower + // 3. borrow half of available liquidity from 2nd borrower + // 4. collateral price drops + // 5. liquidate both troves + function test_liquidateTroves( + uint256 _amount + ) public { + _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); + + // Lend some from lender + mintAndDepositIntoLender(userLender, _amount); + + uint256 _halfAmount = _amount / 2; + + // Calculate how much collateral is needed for the borrow amount + uint256 _collateralNeeded = + (_halfAmount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + // Calculate expected debt (borrow amount + upfront fee) + uint256 _expectedDebt = _halfAmount + troveManager.get_upfront_fee(_halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + + // Open a trove for the first borrower + uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + + // Check trove info + ITroveManager.Trove memory _trove = troveManager.troves(_troveId); + assertEq(_trove.debt, _expectedDebt, "E0"); + assertEq(_trove.collateral, _collateralNeeded, "E1"); + assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E2"); + assertEq(_trove.last_debt_update_time, block.timestamp, "E3"); + assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E4"); + assertEq(_trove.owner, userBorrower, "E5"); + assertEq(_trove.pending_owner, address(0), "E6"); + assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E7"); + assertApproxEqRel( + (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, + DEFAULT_TARGET_COLLATERAL_RATIO, + 1e15, + "E8" + ); // 0.1% + + // Check sorted troves + assertFalse(sortedTroves.empty(), "E9"); + assertEq(sortedTroves.size(), 1, "E10"); + assertEq(sortedTroves.first(), _troveId, "E11"); + assertEq(sortedTroves.last(), _troveId, "E12"); + assertTrue(sortedTroves.contains(_troveId), "E13"); + + // Check balances + assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded, "E14"); + assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E15"); + assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E16"); + assertApproxEqAbs(borrowToken.balanceOf(address(lender)), _halfAmount, 1, "E17"); + assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E18"); + + // Check global info + assertEq(troveManager.total_debt(), _expectedDebt, "E19"); + assertEq(troveManager.total_weighted_debt(), _expectedDebt * DEFAULT_ANNUAL_INTEREST_RATE, "E20"); + assertEq(troveManager.collateral_balance(), _collateralNeeded, "E21"); + assertEq(troveManager.zombie_trove_id(), 0, "E22"); + + // Check dutch desk is empty + assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E23"); + assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E24"); + + // Open a trove for the second borrower + uint256 _anotherTroveId = mintAndOpenTrove(anotherUserBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + + // Check trove info + _trove = troveManager.troves(_anotherTroveId); + assertEq(_trove.debt, _expectedDebt, "E25"); + assertEq(_trove.collateral, _collateralNeeded, "E26"); + assertEq(_trove.annual_interest_rate, DEFAULT_ANNUAL_INTEREST_RATE, "E27"); + assertEq(_trove.last_debt_update_time, block.timestamp, "E28"); + assertEq(_trove.last_interest_rate_adj_time, block.timestamp, "E29"); + assertEq(_trove.owner, anotherUserBorrower, "E30"); + assertEq(_trove.pending_owner, address(0), "E31"); + assertEq(uint256(_trove.status), uint256(ITroveManager.Status.active), "E32"); + assertApproxEqRel( + (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt, + DEFAULT_TARGET_COLLATERAL_RATIO, + 1e15, + "E33" + ); // 0.1% + + // Check sorted troves + assertFalse(sortedTroves.empty(), "E34"); + assertEq(sortedTroves.size(), 2, "E35"); + assertEq(sortedTroves.first(), _troveId, "E36"); + assertEq(sortedTroves.last(), _anotherTroveId, "E37"); + assertTrue(sortedTroves.contains(_troveId), "E38"); + assertTrue(sortedTroves.contains(_anotherTroveId), "E39"); + + // Check balances + assertEq(collateralToken.balanceOf(address(troveManager)), _collateralNeeded * 2, "E40"); + assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E41"); + assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E42"); + assertApproxEqAbs(borrowToken.balanceOf(address(lender)), 0, 1, "E43"); + assertEq(borrowToken.balanceOf(anotherUserBorrower), _halfAmount, "E44"); + + // Check global info + assertEq(troveManager.total_debt(), _expectedDebt * 2, "E45"); + assertEq(troveManager.total_weighted_debt(), _expectedDebt * 2 * DEFAULT_ANNUAL_INTEREST_RATE, "E46"); + assertEq(troveManager.collateral_balance(), _collateralNeeded * 2, "E47"); + assertEq(troveManager.zombie_trove_id(), 0, "E48"); + + // Check dutch desk is empty + assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E49"); + assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E50"); + + // CR = (collateral * price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / debt + // So price_at_MCR = MCR * debt * ORACLE_PRICE_SCALE / (collateral * BORROW_TOKEN_PRECISION) + // We want to be 1% below MCR + uint256 _priceDropToBelowMCR; + if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { + // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow + _priceDropToBelowMCR = + troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); + } else { + // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow + _priceDropToBelowMCR = + troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; + } + uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); + + // Drop collateral price to put trove below MCR + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); + + // Make sure price actually dropped + assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E51"); + + // Calculate Trove's collateral ratio after price drop + uint256 _troveCollateralRatioAfter = (_trove.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; + + // Make sure Trove is below MCR + assertLt(_troveCollateralRatioAfter, troveManager.minimum_collateral_ratio(), "E52"); + + // Calculate expected collateral to decrease (same for both troves) + uint256 _expectedCollateralToDecrease = + calculateCollateralToDecrease(_troveCollateralRatioAfter, _expectedDebt, _priceDropToBelowMCR, _collateralNeeded); + uint256 _expectedRemainingCollateral = _collateralNeeded - _expectedCollateralToDecrease; + + // Finally, liquidate both troves + liquidate(_troveId); + liquidate(_anotherTroveId); + + // Make sure lender got all the borrow tokens back + assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt * 2, "E53"); + + // Make sure liquidator mock got the collateral + assertEq(collateralToken.balanceOf(address(liquidatorMock)), _expectedCollateralToDecrease * 2, "E54"); + + // Check everything again + + // Check trove info + _trove = troveManager.troves(_troveId); + assertEq(_trove.debt, 0, "E55"); + assertEq(_trove.collateral, 0, "E56"); + assertEq(_trove.annual_interest_rate, 0, "E57"); + assertEq(_trove.last_debt_update_time, 0, "E58"); + assertEq(_trove.last_interest_rate_adj_time, 0, "E59"); + assertEq(_trove.owner, address(0), "E60"); + assertEq(_trove.pending_owner, address(0), "E61"); + assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E62"); + + // Check sorted troves + assertTrue(sortedTroves.empty(), "E63"); + assertEq(sortedTroves.size(), 0, "E64"); + assertEq(sortedTroves.first(), 0, "E65"); + assertEq(sortedTroves.last(), 0, "E66"); + assertFalse(sortedTroves.contains(_troveId), "E67"); + + // Check balances + assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E68"); + assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E69"); + assertEq(collateralToken.balanceOf(address(userBorrower)), _expectedRemainingCollateral, "E70"); + assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E71"); + assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E72"); + assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E73"); + + // Check global info + assertEq(troveManager.total_debt(), 0, "E74"); + assertEq(troveManager.total_weighted_debt(), 0, "E75"); + assertEq(troveManager.collateral_balance(), 0, "E76"); + assertEq(troveManager.zombie_trove_id(), 0, "E77"); + + // Check dutch desk is empty + assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E78"); + assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E79"); + + // Check everything again for the second trove + + // Check trove info + _trove = troveManager.troves(_anotherTroveId); + assertEq(_trove.debt, 0, "E80"); + assertEq(_trove.collateral, 0, "E81"); + assertEq(_trove.annual_interest_rate, 0, "E82"); + assertEq(_trove.last_debt_update_time, 0, "E83"); + assertEq(_trove.last_interest_rate_adj_time, 0, "E84"); + assertEq(_trove.owner, address(0), "E85"); + assertEq(_trove.pending_owner, address(0), "E86"); + assertEq(uint256(_trove.status), uint256(ITroveManager.Status.liquidated), "E87"); + + // Check sorted troves + assertTrue(sortedTroves.empty(), "E88"); + assertEq(sortedTroves.size(), 0, "E89"); + assertEq(sortedTroves.first(), 0, "E90"); + assertEq(sortedTroves.last(), 0, "E91"); + assertFalse(sortedTroves.contains(_anotherTroveId), "E92"); + + // Check balances + assertEq(collateralToken.balanceOf(address(troveManager)), 0, "E93"); + assertEq(collateralToken.balanceOf(address(troveManager)), troveManager.collateral_balance(), "E94"); + assertEq(collateralToken.balanceOf(address(userBorrower)), _expectedRemainingCollateral, "E95"); + assertEq(borrowToken.balanceOf(address(troveManager)), 0, "E96"); + assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt, "E97"); + assertEq(borrowToken.balanceOf(userBorrower), _halfAmount, "E98"); + + // Check global info + assertEq(troveManager.total_debt(), 0, "E99"); + assertEq(troveManager.total_weighted_debt(), 0, "E100"); + assertEq(troveManager.collateral_balance(), 0, "E101"); + assertEq(troveManager.zombie_trove_id(), 0, "E102"); + + // Check dutch desk is empty + assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E103"); + assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E104"); + } + + // 1. lend + // 2. open 2 troves + // 3. collateral price drops + // 4. liquidate first trove + // 5. liquidate second trove + function test_liquidateTroves_sequentialLiquidations( + uint256 _amount + ) public { + _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); + + // Lend some from lender + mintAndDepositIntoLender(userLender, _amount); + + uint256 _halfAmount = _amount / 2; + + // Calculate how much collateral is needed for the borrow amount + uint256 _collateralNeeded = + (_halfAmount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + // Calculate expected debt (borrow amount + upfront fee) + uint256 _expectedDebt = _halfAmount + troveManager.get_upfront_fee(_halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + + // Open first trove + uint256 _troveId1 = mintAndOpenTrove(userBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + + // Open second trove + uint256 _troveId2 = mintAndOpenTrove(anotherUserBorrower, _collateralNeeded, _halfAmount, DEFAULT_ANNUAL_INTEREST_RATE); + + // Check trove info for first trove + ITroveManager.Trove memory _trove1 = troveManager.troves(_troveId1); + assertEq(_trove1.debt, _expectedDebt, "E0"); + assertEq(_trove1.collateral, _collateralNeeded, "E1"); + + // Check trove info for second trove + ITroveManager.Trove memory _trove2 = troveManager.troves(_troveId2); + assertEq(_trove2.debt, _expectedDebt, "E2"); + assertEq(_trove2.collateral, _collateralNeeded, "E3"); + + // Check dutch desk is empty + assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E4"); + assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E5"); + + // CR = collateral * price / debt, so price_at_MCR = MCR * debt / collateral + // We want to be 1% below MCR + uint256 _priceDropToBelowMCR; + if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { + // For low-decimal borrow tokens (e.g., USDC 6d), multiply first to avoid underflow + _priceDropToBelowMCR = troveManager.minimum_collateral_ratio() * _trove1.debt * ORACLE_PRICE_SCALE * 99 + / (100 * _trove1.collateral * BORROW_TOKEN_PRECISION); + } else { + // For high-decimal borrow tokens (e.g., crvUSD 18d), divide first to avoid overflow + _priceDropToBelowMCR = + troveManager.minimum_collateral_ratio() * _trove1.debt * 99 / 100 / _trove1.collateral * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION; + } + uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); + + // Drop collateral price to put both troves below MCR + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); + + // Make sure price actually dropped + assertEq(priceOracle.get_price(), _priceDropToBelowMCR, "E6"); + + // Make sure both troves are below MCR + assertLt( + (_trove1.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove1.debt, + troveManager.minimum_collateral_ratio(), + "E7" + ); + assertLt( + (_trove2.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove2.debt, + troveManager.minimum_collateral_ratio(), + "E8" + ); + + // Calculate expected collateral to decrease (same for both troves) + uint256 _troveCollateralRatioAfter = + (_trove1.collateral * priceOracle.get_price() / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove1.debt; + uint256 _expectedCollateralToDecrease = + calculateCollateralToDecrease(_troveCollateralRatioAfter, _expectedDebt, _priceDropToBelowMCR, _collateralNeeded); + + // Liquidate the first trove + liquidate(_troveId1); + + // Check liquidator mock received collateral from first trove + assertEq(collateralToken.balanceOf(address(liquidatorMock)), _expectedCollateralToDecrease, "E9"); + + // Check first trove is liquidated + assertEq(uint256(troveManager.troves(_troveId1).status), uint256(ITroveManager.Status.liquidated), "E10"); + + // Check second trove is still active + assertEq(uint256(troveManager.troves(_troveId2).status), uint256(ITroveManager.Status.active), "E11"); + + // Liquidate the second trove + liquidate(_troveId2); + + // Check second trove is now liquidated + assertEq(uint256(troveManager.troves(_troveId2).status), uint256(ITroveManager.Status.liquidated), "E12"); + + // Make sure lender got all the borrow tokens back + assertGe(borrowToken.balanceOf(address(lender)), _expectedDebt * 2, "E13"); + + // Make sure liquidator mock got all the collateral + assertEq(collateralToken.balanceOf(address(liquidatorMock)), _expectedCollateralToDecrease * 2, "E14"); + + // Check dutch desk is empty + assertEq(borrowToken.balanceOf(address(dutchDesk)), 0, "E15"); + assertEq(collateralToken.balanceOf(address(dutchDesk)), 0, "E16"); + + // Check global info + assertEq(troveManager.total_debt(), 0, "E17"); + assertEq(troveManager.total_weighted_debt(), 0, "E18"); + assertEq(troveManager.collateral_balance(), 0, "E19"); + } + + // Liquidate 4 troves at different price levels to test fee scaling: + // 1. Near MCR --> min fee (0.5%) + // 2. Midpoint between MCR and max penalty CR --> interpolated fee (~max/2) + // 3. At max penalty CR --> max fee (5%) + // 4. Price back up near MCR --> min fee (0.5%) again + function test_liquidateTrove_feeScaling() public { + uint256 _minDebt = troveManager.min_debt(); + uint256 _collateralNeeded = + (_minDebt * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + mintAndDepositIntoLender(userLender, _minDebt * 4); + + uint256 _troveId1 = mintAndOpenTrove(address(1001), _collateralNeeded, _minDebt, DEFAULT_ANNUAL_INTEREST_RATE); + uint256 _troveId2 = mintAndOpenTrove(address(1002), _collateralNeeded, _minDebt, DEFAULT_ANNUAL_INTEREST_RATE); + uint256 _troveId3 = mintAndOpenTrove(address(1003), _collateralNeeded, _minDebt, DEFAULT_ANNUAL_INTEREST_RATE); + uint256 _troveId4 = mintAndOpenTrove(address(1004), _collateralNeeded, _minDebt, DEFAULT_ANNUAL_INTEREST_RATE); + + ITroveManager.Trove memory _trove = troveManager.troves(_troveId1); + uint256 _mcr = troveManager.minimum_collateral_ratio(); + uint256 _maxPenaltyCR = troveManager.max_penalty_collateral_ratio(); + + // Price levels for different fee tiers + uint256 _priceNearMCR = (_mcr - 1) * _trove.debt * ORACLE_PRICE_SCALE / (_trove.collateral * BORROW_TOKEN_PRECISION); + uint256 _priceMid = ((_mcr + _maxPenaltyCR) / 2) * _trove.debt * ORACLE_PRICE_SCALE / (_trove.collateral * BORROW_TOKEN_PRECISION); + uint256 _priceMaxPenalty = _maxPenaltyCR * _trove.debt * ORACLE_PRICE_SCALE / (_trove.collateral * BORROW_TOKEN_PRECISION); + + uint256 _balBefore; + uint256 _baseCollateral; + + // --- Trove 1: CR just below MCR --> min fee (0.5%) --- + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceNearMCR)); + vm.mockCall( + address(priceOracle), + abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), + abi.encode(_priceNearMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION)) + ); + _balBefore = collateralToken.balanceOf(address(liquidatorMock)); + liquidate(_troveId1); + uint256 _received1 = collateralToken.balanceOf(address(liquidatorMock)) - _balBefore; + _baseCollateral = _trove.debt * ORACLE_PRICE_SCALE / _priceNearMCR; + assertApproxEqRel( + _received1, _baseCollateral * (BORROW_TOKEN_PRECISION + troveManager.min_liquidation_fee()) / BORROW_TOKEN_PRECISION, 1e15, "E0" + ); + + // --- Trove 2: CR at midpoint --> interpolated fee (~max/2) --- + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceMid)); + vm.mockCall( + address(priceOracle), + abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), + abi.encode(_priceMid * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION)) + ); + _balBefore = collateralToken.balanceOf(address(liquidatorMock)); + liquidate(_troveId2); + uint256 _received2 = collateralToken.balanceOf(address(liquidatorMock)) - _balBefore; + _baseCollateral = _trove.debt * ORACLE_PRICE_SCALE / _priceMid; + assertApproxEqRel( + _received2, _baseCollateral * (BORROW_TOKEN_PRECISION + troveManager.max_liquidation_fee() / 2) / BORROW_TOKEN_PRECISION, 1e16, "E1" + ); + + // --- Trove 3: CR at max penalty --> max fee (5%) --- + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceMaxPenalty)); + vm.mockCall( + address(priceOracle), + abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), + abi.encode(_priceMaxPenalty * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION)) + ); + _balBefore = collateralToken.balanceOf(address(liquidatorMock)); + liquidate(_troveId3); + uint256 _received3 = collateralToken.balanceOf(address(liquidatorMock)) - _balBefore; + _baseCollateral = _trove.debt * ORACLE_PRICE_SCALE / _priceMaxPenalty; + assertEq(_received3, _baseCollateral * (BORROW_TOKEN_PRECISION + troveManager.max_liquidation_fee()) / BORROW_TOKEN_PRECISION, "E2"); + + // --- Trove 4: price back up near MCR --> min fee (0.5%) again --- + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceNearMCR)); + vm.mockCall( + address(priceOracle), + abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), + abi.encode(_priceNearMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION)) + ); + _balBefore = collateralToken.balanceOf(address(liquidatorMock)); + liquidate(_troveId4); + uint256 _received4 = collateralToken.balanceOf(address(liquidatorMock)) - _balBefore; + + // Verify fee scaling: more collateral at lower CRs + assertLt(_received1, _received2, "E3"); + assertLt(_received2, _received3, "E4"); + assertEq(_received1, _received4, "E5"); + } + + // Partial liquidation: liquidate 1/4 of the debt, trove stays open with improved CR + function test_liquidateTrove_partial( + uint256 _amount + ) public { + _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); + + mintAndDepositIntoLender(userLender, _amount); + + uint256 _collateralNeeded = + (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + + ITroveManager.Trove memory _trove = troveManager.troves(_troveId); + + // Drop price to 1% below MCR + uint256 _price; + if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { + _price = + troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); + } else { + _price = + troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; + } + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_price)); + vm.mockCall( + address(priceOracle), + abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), + abi.encode(_price * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION)) + ); + + uint256 _crBefore = (_trove.collateral * _price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; + assertLt(_crBefore, troveManager.minimum_collateral_ratio(), "E0"); + + // Partial liquidation: liquidate 1/4 of the debt + uint256 _debtToLiquidate = _trove.debt / 4; + uint256 _expectedCollateral = calculateCollateralToDecrease(_crBefore, _debtToLiquidate, _price, _trove.collateral); + + liquidatorMock.liquidate(_troveId, _debtToLiquidate); + + // Trove should still be active with reduced debt and collateral + ITroveManager.Trove memory _troveAfter = troveManager.troves(_troveId); + assertEq(uint256(_troveAfter.status), uint256(ITroveManager.Status.active), "E1"); + assertEq(_troveAfter.debt, _trove.debt - _debtToLiquidate, "E2"); + assertEq(_troveAfter.collateral, _trove.collateral - _expectedCollateral, "E3"); + assertEq(_troveAfter.owner, userBorrower, "E4"); + + // CR should have improved and be above MCR + uint256 _crAfter = (_troveAfter.collateral * _price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _troveAfter.debt; + assertGt(_crAfter, _crBefore, "E5"); + assertGe(_crAfter, troveManager.minimum_collateral_ratio(), "E6"); + + // Liquidator received correct collateral + assertEq(collateralToken.balanceOf(address(liquidatorMock)), _expectedCollateral, "E7"); + + // Trove owner did not receive collateral (only happens on full liquidation) + assertEq(collateralToken.balanceOf(userBorrower), 0, "E8"); + + // Trove still in sorted list + assertTrue(sortedTroves.contains(_troveId), "E9"); + } + + // Partial liquidation that triggers full: remaining debt would be below min_debt + function test_liquidateTrove_partialBecomesFullBelowMinDebt( + uint256 _amount + ) public { + _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); + + mintAndDepositIntoLender(userLender, _amount); + + uint256 _collateralNeeded = + (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + + ITroveManager.Trove memory _trove = troveManager.troves(_troveId); + + // Drop price to 1% below MCR + uint256 _price; + if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { + _price = + troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); + } else { + _price = + troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; + } + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_price)); + vm.mockCall( + address(priceOracle), + abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), + abi.encode(_price * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION)) + ); + + // Try partial: amount that would leave remaining below min_debt --> forced full + uint256 _partialDebt = _trove.debt - troveManager.min_debt() + 1; + + // Expected collateral for full liquidation (full debt, not partial) + uint256 _cr = (_trove.collateral * _price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; + uint256 _expectedCollateral = calculateCollateralToDecrease(_cr, _trove.debt, _price, _trove.collateral); + uint256 _expectedRemaining = _trove.collateral - _expectedCollateral; + + liquidatorMock.liquidate(_troveId, _partialDebt); + + // Trove should be fully liquidated (not partial) + ITroveManager.Trove memory _troveAfter = troveManager.troves(_troveId); + assertEq(uint256(_troveAfter.status), uint256(ITroveManager.Status.liquidated), "E0"); + assertEq(_troveAfter.debt, 0, "E1"); + assertEq(_troveAfter.collateral, 0, "E2"); + + // Liquidator got the collateral_to_decrease (for full debt, not the partial amount) + assertEq(collateralToken.balanceOf(address(liquidatorMock)), _expectedCollateral, "E3"); + + // Trove owner got the remaining collateral + assertEq(collateralToken.balanceOf(userBorrower), _expectedRemaining, "E4"); + + // Trove removed from sorted list + assertFalse(sortedTroves.contains(_troveId), "E5"); + } + + function test_liquidateTrove_nonExistentTrove( + uint256 _nonExistentTroveId + ) public { + _nonExistentTroveId = bound(_nonExistentTroveId, 1, type(uint256).max); + // Make sure we always fail when a non-existent trove is passed + vm.expectRevert("!active or zombie"); + liquidatorMock.liquidate(_nonExistentTroveId, type(uint256).max); + } + + function test_liquidateTrove_aboveMCR( + uint256 _amount + ) public { + _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); + + // Lend some from lender + mintAndDepositIntoLender(userLender, _amount); + + // Calculate how much collateral is needed for the borrow amount + uint256 _collateralNeeded = + (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + // Open a trove + uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + + // Make sure we cannot liquidate a trove that is above MCR + vm.expectRevert("!collateral_ratio"); + liquidatorMock.liquidate(_troveId, type(uint256).max); + } + + function test_liquidateGas() public { + uint256 _amount = troveManager.min_debt() * BORROW_TOKEN_PRECISION; + + mintAndDepositIntoLender(userLender, _amount); + + uint256 _collateralNeeded = + (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + + // Drop price below MCR + ITroveManager.Trove memory _trove = troveManager.troves(_troveId); + uint256 _priceDropToBelowMCR = + troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); + uint256 _priceDropToBelowMCR18 = _priceDropToBelowMCR * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION); + + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_priceDropToBelowMCR)); + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), abi.encode(_priceDropToBelowMCR18)); + + uint256 _gasBefore = gasleft(); + liquidate(_troveId); + uint256 _gasUsed = _gasBefore - gasleft(); + + console2.log("Gas used to liquidate 1 trove:", _gasUsed); + } + +} diff --git a/test/TroveManager.t.sol b/test/TroveManager.t.sol index 97dcee2..5f33159 100644 --- a/test/TroveManager.t.sol +++ b/test/TroveManager.t.sol @@ -20,15 +20,18 @@ contract TroveManagerTests is Base { assertEq(troveManager.borrow_token_precision(), BORROW_TOKEN_PRECISION, "E7"); assertEq(troveManager.min_debt(), minimumDebt * BORROW_TOKEN_PRECISION, "E8"); assertEq(troveManager.minimum_collateral_ratio(), minimumCollateralRatio * BORROW_TOKEN_PRECISION / 100, "E9"); - assertEq(troveManager.upfront_interest_period(), upfrontInterestPeriod, "E10"); - assertEq(troveManager.interest_rate_adj_cooldown(), interestRateAdjCooldown, "E11"); - assertEq(troveManager.min_annual_interest_rate(), BORROW_TOKEN_PRECISION / 100 / 2, "E12"); // 0.5% - assertEq(troveManager.max_annual_interest_rate(), 250 * BORROW_TOKEN_PRECISION / 100, "E13"); // 250% - assertEq(troveManager.zombie_trove_id(), 0, "E14"); - assertEq(troveManager.total_debt(), 0, "E15"); - assertEq(troveManager.total_weighted_debt(), 0, "E16"); - assertEq(troveManager.last_debt_update_time(), 0, "E17"); - assertEq(troveManager.collateral_balance(), 0, "E18"); + assertEq(troveManager.max_penalty_collateral_ratio(), maxPenaltyCollateralRatio * BORROW_TOKEN_PRECISION / 100, "E10"); + assertEq(troveManager.min_liquidation_fee(), minLiquidationFee * BORROW_TOKEN_PRECISION / 100 / 100, "E11"); + assertEq(troveManager.max_liquidation_fee(), maxLiquidationFee * BORROW_TOKEN_PRECISION / 100 / 100, "E12"); + assertEq(troveManager.upfront_interest_period(), upfrontInterestPeriod, "E13"); + assertEq(troveManager.interest_rate_adj_cooldown(), interestRateAdjCooldown, "E14"); + assertEq(troveManager.min_annual_interest_rate(), BORROW_TOKEN_PRECISION / 100 / 2, "E15"); // 0.5% + assertEq(troveManager.max_annual_interest_rate(), 250 * BORROW_TOKEN_PRECISION / 100, "E16"); // 250% + assertEq(troveManager.zombie_trove_id(), 0, "E17"); + assertEq(troveManager.total_debt(), 0, "E18"); + assertEq(troveManager.total_weighted_debt(), 0, "E19"); + assertEq(troveManager.last_debt_update_time(), 0, "E20"); + assertEq(troveManager.collateral_balance(), 0, "E21"); } function test_initialize_revertsIfAlreadyInitialized() public { @@ -43,6 +46,9 @@ contract TroveManagerTests is Base { collateral_token: address(collateralToken), minimum_debt: minimumDebt, minimum_collateral_ratio: minimumCollateralRatio, + max_penalty_collateral_ratio: maxPenaltyCollateralRatio, + min_liquidation_fee: minLiquidationFee, + max_liquidation_fee: maxLiquidationFee, upfront_interest_period: upfrontInterestPeriod, interest_rate_adj_cooldown: interestRateAdjCooldown }) diff --git a/test/interfaces/ITroveManager.sol b/test/interfaces/ITroveManager.sol index 24314ad..5f7f009 100644 --- a/test/interfaces/ITroveManager.sol +++ b/test/interfaces/ITroveManager.sol @@ -44,6 +44,9 @@ interface ITroveManager { address collateral_token; uint256 minimum_debt; uint256 minimum_collateral_ratio; + uint256 max_penalty_collateral_ratio; + uint256 min_liquidation_fee; + uint256 max_liquidation_fee; uint256 upfront_interest_period; uint256 interest_rate_adj_cooldown; } @@ -67,6 +70,9 @@ interface ITroveManager { function borrow_token_precision() external view returns (uint256); function min_debt() external view returns (uint256); function minimum_collateral_ratio() external view returns (uint256); + function max_penalty_collateral_ratio() external view returns (uint256); + function min_liquidation_fee() external view returns (uint256); + function max_liquidation_fee() external view returns (uint256); function upfront_interest_period() external view returns (uint256); function interest_rate_adj_cooldown() external view returns (uint256); function min_annual_interest_rate() external view returns (uint256); diff --git a/test/mocks/LiquidatorMock.sol b/test/mocks/LiquidatorMock.sol new file mode 100644 index 0000000..f95d592 --- /dev/null +++ b/test/mocks/LiquidatorMock.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {ITroveManager} from "../interfaces/ITroveManager.sol"; + +import "forge-std/Test.sol"; + +contract LiquidatorMock is Test { + + ITroveManager public immutable troveManager; + IERC20 public immutable borrowToken; + + constructor( + ITroveManager _troveManager, + IERC20 _borrowToken + ) { + troveManager = _troveManager; + borrowToken = _borrowToken; + } + + function liquidate( + uint256 _troveId, + uint256 _maxAmount + ) external returns (uint256) { + return troveManager.liquidate_trove(_troveId, _maxAmount, address(this), abi.encode(uint256(420))); + } + + function takeCallback( + uint256, + address, + uint256, + uint256 _neededAmount, + bytes calldata + ) external { + deal(address(borrowToken), address(this), _neededAmount); + borrowToken.approve(msg.sender, _neededAmount); + } + +} From 4a8295ce0006f810bb9e1a3d7a5805816a89a897 Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Sun, 15 Feb 2026 19:48:01 -0300 Subject: [PATCH 5/9] chore: add underline to numbers --- src/sorted_troves.vy | 2 +- src/trove_manager.vy | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sorted_troves.vy b/src/sorted_troves.vy index 3414251..6447902 100644 --- a/src/sorted_troves.vy +++ b/src/sorted_troves.vy @@ -36,7 +36,7 @@ struct Trove: _ROOT_TROVE_ID: constant(uint256) = 0 _BAD_HINT: constant(uint256) = 0 -_MAX_TROVES: constant(uint256) = 10000 +_MAX_TROVES: constant(uint256) = 10_000 # ============================================================================================ diff --git a/src/trove_manager.vy b/src/trove_manager.vy index b236a7b..5c6309b 100644 --- a/src/trove_manager.vy +++ b/src/trove_manager.vy @@ -154,9 +154,9 @@ struct InitializeParams: _MAX_CALLBACK_DATA_SIZE: constant(uint256) = 10**5 _PRICE_ORACLE_PRECISION: constant(uint256) = 10 ** 36 -_LIQUIDATION_FEE_PRECISION: constant(uint256) = 10000 +_LIQUIDATION_FEE_PRECISION: constant(uint256) = 10_000 _WAD: constant(uint256) = 10 ** 18 -_MAX_REDEMPTIONS: constant(uint256) = 1000 +_MAX_REDEMPTIONS: constant(uint256) = 1_000 _ONE_YEAR: constant(uint256) = 365 * 60 * 60 * 24 From 39be516be0afcd3f5159886c0957d7216164f532 Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Mon, 16 Feb 2026 20:35:02 -0300 Subject: [PATCH 6/9] feat: cap by safe collateral ratio --- script/interfaces/ICatFactory.sol | 1 + src/factory.vy | 2 + src/interfaces/ITroveManager.vyi | 1 + src/trove_manager.vy | 164 +++++++++++----------- test/Base.sol | 2 + test/Liquidate.t.sol | 223 +++++++++++++++++++++++++++++- test/TroveManager.t.sol | 1 + test/interfaces/ITroveManager.sol | 4 +- 8 files changed, 312 insertions(+), 86 deletions(-) diff --git a/script/interfaces/ICatFactory.sol b/script/interfaces/ICatFactory.sol index fcc16d2..d27d256 100644 --- a/script/interfaces/ICatFactory.sol +++ b/script/interfaces/ICatFactory.sol @@ -14,6 +14,7 @@ interface ICatFactory { address management; address performance_fee_recipient; uint256 minimum_debt; + uint256 safe_collateral_ratio; uint256 minimum_collateral_ratio; uint256 max_penalty_collateral_ratio; uint256 min_liquidation_fee; diff --git a/src/factory.vy b/src/factory.vy index ef9084d..4807f2c 100644 --- a/src/factory.vy +++ b/src/factory.vy @@ -42,6 +42,7 @@ struct DeployParams: management: address # address of the management performance_fee_recipient: address # address of the performance fee recipient minimum_debt: uint256 # minimum borrowable amount, e.g., `500 * borrow_token_precision` for 500 tokens + safe_collateral_ratio: uint256 # target CR after partial liquidation, e.g., `115` for 115%. must be greater than `100% + max_liquidation_fee` to avoid underflow in the safe CR calculation minimum_collateral_ratio: uint256 # minimum CR to avoid liquidation, e.g., `110` for 110% max_penalty_collateral_ratio: uint256 # CR at which max liquidation fee applies, e.g., `105` for 105% min_liquidation_fee: uint256 # minimum liquidation fee in hundredths of a percent, e.g., `50` for 0.5% @@ -155,6 +156,7 @@ def deploy(params: DeployParams) -> (address, address, address, address, address borrow_token=params.borrow_token, collateral_token=params.collateral_token, minimum_debt=params.minimum_debt, + safe_collateral_ratio=params.safe_collateral_ratio, minimum_collateral_ratio=params.minimum_collateral_ratio, max_penalty_collateral_ratio=params.max_penalty_collateral_ratio, min_liquidation_fee=params.min_liquidation_fee, diff --git a/src/interfaces/ITroveManager.vyi b/src/interfaces/ITroveManager.vyi index 556e581..74926c7 100644 --- a/src/interfaces/ITroveManager.vyi +++ b/src/interfaces/ITroveManager.vyi @@ -37,6 +37,7 @@ struct InitializeParams: borrow_token: address collateral_token: address minimum_debt: uint256 + safe_collateral_ratio: uint256 minimum_collateral_ratio: uint256 max_penalty_collateral_ratio: uint256 min_liquidation_fee: uint256 diff --git a/src/trove_manager.vy b/src/trove_manager.vy index 5c6309b..19f34a2 100644 --- a/src/trove_manager.vy +++ b/src/trove_manager.vy @@ -139,6 +139,7 @@ struct InitializeParams: borrow_token: address collateral_token: address minimum_debt: uint256 + safe_collateral_ratio: uint256 minimum_collateral_ratio: uint256 max_penalty_collateral_ratio: uint256 min_liquidation_fee: uint256 @@ -179,6 +180,7 @@ collateral_token: public(IERC20) one_pct: public(uint256) borrow_token_precision: public(uint256) min_debt: public(uint256) +safe_collateral_ratio: public(uint256) minimum_collateral_ratio: public(uint256) max_penalty_collateral_ratio: public(uint256) min_liquidation_fee: public(uint256) @@ -232,6 +234,7 @@ def initialize(params: InitializeParams): self.one_pct = one_pct self.borrow_token_precision = borrow_token_precision self.min_debt = params.minimum_debt * borrow_token_precision + self.safe_collateral_ratio = params.safe_collateral_ratio * one_pct self.minimum_collateral_ratio = params.minimum_collateral_ratio * one_pct self.max_penalty_collateral_ratio = params.max_penalty_collateral_ratio * one_pct self.min_liquidation_fee = params.min_liquidation_fee * one_hundredth_pct @@ -957,24 +960,26 @@ def close_zombie_trove(trove_id: uint256): @external def liquidate_trove( trove_id: uint256, - max_debt_to_decrease: uint256 = max_value(uint256), + max_debt_to_repay: uint256 = max_value(uint256), receiver: address = msg.sender, data: Bytes[_MAX_CALLBACK_DATA_SIZE] = empty(Bytes[_MAX_CALLBACK_DATA_SIZE]) ) -> uint256: """ @notice Liquidate a single unhealthy Trove (fully or partially) - @dev The liquidator repays debt and receives the equivalent collateral plus a dynamic bonus + @dev The liquidator repays debt and receives the equivalent collateral plus a dynamic fee that scales with the Trove's collateral ratio. If remaining debt would fall below - `min_debt`, the entire debt is liquidated. Full liquidations close the Trove and - return any excess collateral to the owner. Partial liquidations require the Trove + `min_debt`, the entire debt is repaid. Full liquidations close the Trove and + transfer any excess collateral to the owner. Partial liquidations require the Trove to end up above the minimum collateral ratio. Collateral is sent to the `receiver` - first, then, if `data` is non-empty, a `takeCallback` is then invoked on the `receiver`, + first, then, if `data` is non-empty, a `takeCallback` is invoked on the `receiver`, before debt is pulled @param trove_id Unique identifier of the unhealthy Trove - @param max_debt_to_decrease The maximum amount of debt to liquidate. Defaults to max uint256 + @param max_debt_to_repay Upper bound on debt to repay. May be exceeded when remaining + debt would fall below `min_debt`, forcing full liquidation. Also capped by the + `safe_collateral_ratio` target. Defaults to max uint256 @param receiver The address that will receive the collateral tokens. Defaults to msg.sender @param data The data to pass to the `receiver` callback. Defaults to empty - @return The amount of collateral tokens sent to the `receiver` + @return The amount of liquidated collateral tokens """ # Make sure the trove ID is non-zero assert trove_id != 0, "!trove_id" @@ -1005,24 +1010,66 @@ def liquidate_trove( # Make sure the collateral ratio is below the minimum collateral ratio assert collateral_ratio < minimum_collateral_ratio, "!collateral_ratio" - # Determine the debt amount to decrease: - # - Cap at `max_debt_to_decrease` - # - If remaining debt would fall below `min_debt`, decrease the entire debt - debt_to_decrease: uint256 = min(trove_debt_after_interest, max_debt_to_decrease) - if trove_debt_after_interest - debt_to_decrease < self.min_debt: - debt_to_decrease = trove_debt_after_interest - - # Calculate the collateral to send to the `receiver`, capped at the Trove's collateral - collateral_to_decrease: uint256 = min( - self._calculate_collateral_to_decrease(collateral_ratio, debt_to_decrease, collateral_price), - trove.collateral, - ) + # Determine the liquidation fee percentage based on the collateral ratio: + # - At or above minimum collateral ratio --> minimum fee + # - At or below maximum penalty collateral ratio --> maximum fee + # - Between the two --> linear interpolation + liquidation_fee_pct: uint256 = 0 + if collateral_ratio >= minimum_collateral_ratio: + liquidation_fee_pct = self.min_liquidation_fee + elif collateral_ratio <= self.max_penalty_collateral_ratio: + liquidation_fee_pct = self.max_liquidation_fee + else: + min_liquidation_fee: uint256 = self.min_liquidation_fee + collateral_ratio_range: uint256 = minimum_collateral_ratio - self.max_penalty_collateral_ratio + collateral_ratio_drop: uint256 = minimum_collateral_ratio - collateral_ratio + fee_range: uint256 = self.max_liquidation_fee - min_liquidation_fee + liquidation_fee_pct = min_liquidation_fee + (fee_range * collateral_ratio_drop // collateral_ratio_range) + + # Cache the borrow token precision + borrow_token_precision: uint256 = self.borrow_token_precision + + # Convert the Trove's collateral to its equivalent in borrow tokens + trove_collateral_in_borrow: uint256 = trove.collateral * collateral_price // _PRICE_ORACLE_PRECISION + + # Cache the safe collateral ratio + safe_collateral_ratio: uint256 = self.safe_collateral_ratio + + # Calculate the maximum debt to repay to bring the Trove to `safe_collateral_ratio`. + # + # Derived from setting new_cr = safe_cr after reducing debt by `d` and collateral by `d * (1 + fee)`: + # safe_cr = (cv - d * (1 + fee)) / (D - d) + # Solving for `d`: + # d = (safe_cr * D - cv) / (safe_cr - 1 - fee) + # + # Where cv = trove_collateral_in_borrow, D = trove_debt_after_interest, + # and all values are scaled by borrow_token_precision + debt_to_repay_for_safe_collateral_ratio: uint256 = ( + safe_collateral_ratio * trove_debt_after_interest - trove_collateral_in_borrow * borrow_token_precision + ) // (safe_collateral_ratio - borrow_token_precision - liquidation_fee_pct) + + # Determine the debt amount to repay: + # - Cap at `max_debt_to_repay` + # - Cap at the amount that brings the Trove to `safe_collateral_ratio` + # - If remaining debt would fall below `min_debt`, repay the entire debt + debt_to_repay: uint256 = min(min(trove_debt_after_interest, max_debt_to_repay), debt_to_repay_for_safe_collateral_ratio) + if trove_debt_after_interest - debt_to_repay < self.min_debt: + debt_to_repay = trove_debt_after_interest + + # Convert the debt to repay to its equivalent in collateral tokens + debt_to_repay_in_collateral: uint256 = debt_to_repay * _PRICE_ORACLE_PRECISION // collateral_price + + # Apply the liquidation fee + collateral_with_fee: uint256 = debt_to_repay_in_collateral * (borrow_token_precision + liquidation_fee_pct) // borrow_token_precision + + # Determine how much collateral to liquidate, cap at the Trove's collateral + collateral_to_liquidate: uint256 = min(collateral_with_fee, trove.collateral) # Cache the trove owner before the trove is potentially emptied trove_owner: address = trove.owner # Check if this is a full liquidation and cache the result - is_full_liquidation: bool = debt_to_decrease == trove_debt_after_interest + is_full_liquidation: bool = debt_to_repay == trove_debt_after_interest # Full liquidation: close the Trove and transfer any remaining collateral to the owner. # Partial liquidation: reduce the Trove's debt and collateral and keep it open. @@ -1049,7 +1096,7 @@ def liquidate_trove( # Accrue interest on the total debt and update accounting self._accrue_interest_and_account_for_trove_change( 0, # debt_increase - debt_to_decrease, # debt_decrease + debt_to_repay, # debt_decrease 0, # weighted_debt_increase trove_weighted_debt, # weighted_debt_decrease ) @@ -1058,21 +1105,21 @@ def liquidate_trove( if is_active: extcall self.sorted_troves.remove(trove_id) - # Calculate the remaining collateral to return to the trove owner (if any) - remaining_collateral: uint256 = trove_collateral - collateral_to_decrease + # Calculate the remaining collateral to transfer to the Trove owner (if any) + remaining_collateral: uint256 = trove_collateral - collateral_to_liquidate - # If needed, send the remaining collateral tokens to trove owner + # If needed, transfer the remaining collateral tokens to Trove owner if remaining_collateral > 0: assert extcall self.collateral_token.transfer(trove_owner, remaining_collateral, default_return_value=True) else: # Calculate the new debt amount - new_debt: uint256 = trove_debt_after_interest - debt_to_decrease + new_debt: uint256 = trove_debt_after_interest - debt_to_repay # Cache the Trove's old debt for global accounting old_debt: uint256 = trove.debt # Calculate the new collateral amount and collateral ratio - new_collateral: uint256 = trove.collateral - collateral_to_decrease + new_collateral: uint256 = trove.collateral - collateral_to_liquidate new_collateral_ratio: uint256 = self._calculate_collateral_ratio(new_collateral, new_debt, collateral_price) # Make sure the new collateral ratio is above the minimum collateral ratio @@ -1087,44 +1134,44 @@ def liquidate_trove( self.troves[trove_id] = trove # Update the contract's recorded collateral balance - self.collateral_balance -= collateral_to_decrease + self.collateral_balance -= collateral_to_liquidate # Accrue interest on the total debt and update accounting self._accrue_interest_and_account_for_trove_change( 0, # debt_increase - debt_to_decrease, # debt_decrease + debt_to_repay, # debt_decrease new_debt * trove.annual_interest_rate, # weighted_debt_increase old_debt * trove.annual_interest_rate, # weighted_debt_decrease ) - # Send the collateral tokens to the `receiver` - assert extcall self.collateral_token.transfer(receiver, collateral_to_decrease, default_return_value=True) + # Transfer the collateral tokens to the `receiver` + assert extcall self.collateral_token.transfer(receiver, collateral_to_liquidate, default_return_value=True) # If the caller provided data, perform the callback if len(data) != 0: extcall ITaker(receiver).takeCallback( trove_id, msg.sender, - collateral_to_decrease, # amount_taken - debt_to_decrease, # needed_amount + collateral_to_liquidate, # amount_taken + debt_to_repay, # needed_amount data, ) # Pull the borrow tokens from caller and transfer them to the Lender contract - assert extcall self.borrow_token.transferFrom(msg.sender, self.lender, debt_to_decrease, default_return_value=True) + assert extcall self.borrow_token.transferFrom(msg.sender, self.lender, debt_to_repay, default_return_value=True) # Emit event log LiquidateTrove( trove_id=trove_id, trove_owner=trove_owner, liquidator=msg.sender, - collateral_amount=collateral_to_decrease, - debt_amount=debt_to_decrease, + collateral_amount=collateral_to_liquidate, + debt_amount=debt_to_repay, is_full_liquidation=is_full_liquidation, ) # Return the amount of liquidated collateral tokens - return collateral_to_decrease + return collateral_to_liquidate # ============================================================================================ @@ -1352,51 +1399,6 @@ def _calculate_accrued_interest(weighted_debt: uint256, period: uint256) -> uint return weighted_debt * period // _ONE_YEAR // self.borrow_token_precision -@internal -@view -def _calculate_collateral_to_decrease( - collateral_ratio: uint256, - debt_to_decrease: uint256, - collateral_price: uint256, -) -> uint256: - """ - @notice Calculate the amount of collateral to decrease from a Trove on a liquidation - @dev The liquidator receives the debt-equivalent collateral plus a dynamic bonus. - The bonus percentage scales linearly from `min_liquidation_fee` (at minimum collateral ratio) to - `max_liquidation_fee` (at `max_penalty_collateral_ratio`), incentivizing - earlier liquidation of unhealthy troves. - Fee percentages are expressed in borrow token precision (same scale as collateral ratios) - @param collateral_ratio The Trove's current collateral ratio - @param debt_to_decrease The amount of debt being liquidated - @param collateral_price The current collateral price from the oracle - @return The amount of collateral tokens to decrease from the Trove (base + bonus) - """ - # Determine the liquidation fee percentage based on the collateral ratio: - # - At or above minimum collateral ratio --> minimum fee - # - At or below maximum penalty collateral ratio --> maximum fee - # - Between the two --> linear interpolation - liquidation_fee_pct: uint256 = 0 - minimum_collateral_ratio: uint256 = self.minimum_collateral_ratio - if collateral_ratio >= minimum_collateral_ratio: - liquidation_fee_pct = self.min_liquidation_fee - elif collateral_ratio <= self.max_penalty_collateral_ratio: - liquidation_fee_pct = self.max_liquidation_fee - else: - min_liquidation_fee: uint256 = self.min_liquidation_fee - collateral_ratio_range: uint256 = minimum_collateral_ratio - self.max_penalty_collateral_ratio - collateral_ratio_drop: uint256 = minimum_collateral_ratio - collateral_ratio - fee_range: uint256 = self.max_liquidation_fee - min_liquidation_fee - liquidation_fee_pct = min_liquidation_fee + (fee_range * collateral_ratio_drop // collateral_ratio_range) - - # Cache borrow token precision, as `liquidation_fee_pct` is in the same scale - liquidation_fee_precision: uint256 = self.borrow_token_precision - - # Convert the debt to its equivalent in collateral tokens - base_collateral: uint256 = debt_to_decrease * _PRICE_ORACLE_PRECISION // collateral_price - - # Apply the liquidation bonus - return base_collateral * (liquidation_fee_precision + liquidation_fee_pct) // liquidation_fee_precision - @internal @view diff --git a/test/Base.sol b/test/Base.sol index 2fde85c..2e6f98d 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -40,6 +40,7 @@ abstract contract Base is Deploy, Test { // Market parameters uint256 public minimumDebt = 500; // 500 tokens + uint256 public safeCollateralRatio = 115; // 115% uint256 public minimumCollateralRatio = 110; // 110% uint256 public maxPenaltyCollateralRatio = 105; // 105% uint256 public minLiquidationFee = 50; // 0.5% @@ -88,6 +89,7 @@ abstract contract Base is Deploy, Test { management: management, performance_fee_recipient: performanceFeeRecipient, minimum_debt: minimumDebt, + safe_collateral_ratio: safeCollateralRatio, minimum_collateral_ratio: minimumCollateralRatio, max_penalty_collateral_ratio: maxPenaltyCollateralRatio, min_liquidation_fee: minLiquidationFee, diff --git a/test/Liquidate.t.sol b/test/Liquidate.t.sol index 9543a9c..c0e4424 100644 --- a/test/Liquidate.t.sol +++ b/test/Liquidate.t.sol @@ -26,7 +26,8 @@ contract LiquidateTests is Base { function test_liquidateTrove( uint256 _amount ) public { - _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); + // Bound near min_debt to ensure safe CR cap + min_debt check forces full liquidation + _amount = bound(_amount, troveManager.min_debt(), troveManager.min_debt() * 13 / 10); // Lend some from lender mintAndDepositIntoLender(userLender, _amount); @@ -171,7 +172,8 @@ contract LiquidateTests is Base { function test_liquidateTroves( uint256 _amount ) public { - _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); + // Bound near min_debt to ensure safe CR cap + min_debt check forces full liquidation per trove + _amount = bound(_amount, troveManager.min_debt() * 2, troveManager.min_debt() * 26 / 10); // Lend some from lender mintAndDepositIntoLender(userLender, _amount); @@ -402,7 +404,8 @@ contract LiquidateTests is Base { function test_liquidateTroves_sequentialLiquidations( uint256 _amount ) public { - _amount = bound(_amount, troveManager.min_debt() * 2, maxFuzzAmount); + // Bound near min_debt to ensure safe CR cap + min_debt check forces full liquidation per trove + _amount = bound(_amount, troveManager.min_debt() * 2, troveManager.min_debt() * 26 / 10); // Lend some from lender mintAndDepositIntoLender(userLender, _amount); @@ -664,7 +667,8 @@ contract LiquidateTests is Base { function test_liquidateTrove_partialBecomesFullBelowMinDebt( uint256 _amount ) public { - _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); + // Bound near min_debt to ensure safe CR cap doesn't override the explicit partial amount + _amount = bound(_amount, troveManager.min_debt(), troveManager.min_debt() * 13 / 10); mintAndDepositIntoLender(userLender, _amount); @@ -772,4 +776,215 @@ contract LiquidateTests is Base { console2.log("Gas used to liquidate 1 trove:", _gasUsed); } + // Passing max_debt_to_repay = type(uint256).max should cap the liquidation at safe CR + // --> trove stays open with CR ≈ safe_collateral_ratio + function test_liquidateTrove_capsAtSafeCR( + uint256 _amount + ) public { + _amount = bound(_amount, troveManager.min_debt() * 4, maxFuzzAmount); + + mintAndDepositIntoLender(userLender, _amount); + + uint256 _collateralNeeded = + (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + + ITroveManager.Trove memory _trove = troveManager.troves(_troveId); + + // Drop price to 1% below MCR + uint256 _price; + if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { + _price = + troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); + } else { + _price = + troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; + } + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_price)); + vm.mockCall( + address(priceOracle), + abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), + abi.encode(_price * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION)) + ); + + uint256 _crBefore = (_trove.collateral * _price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; + assertLt(_crBefore, troveManager.minimum_collateral_ratio(), "E0"); + + // Liquidate with max_debt = type(uint256).max --> should be capped at safe CR + liquidate(_troveId); + + // Trove should still be active (partial liquidation, not full) + ITroveManager.Trove memory _troveAfter = troveManager.troves(_troveId); + assertEq(uint256(_troveAfter.status), uint256(ITroveManager.Status.active), "E1"); + assertGt(_troveAfter.debt, 0, "E2"); + assertGt(_troveAfter.collateral, 0, "E3"); + + // CR should have improved + uint256 _crAfter = (_troveAfter.collateral * _price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _troveAfter.debt; + assertGt(_crAfter, _crBefore, "E4"); + + // CR should be approximately equal to safe_collateral_ratio + assertApproxEqRel(_crAfter, troveManager.safe_collateral_ratio(), 1e16, "E5"); // 1% tolerance + + // CR should be above MCR + assertGe(_crAfter, troveManager.minimum_collateral_ratio(), "E6"); + + // Trove still in sorted list + assertTrue(sortedTroves.contains(_troveId), "E7"); + + // Trove owner did not receive collateral (partial liquidation) + assertEq(collateralToken.balanceOf(userBorrower), 0, "E8"); + } + + // When max_debt_to_repay < safe CR cap, the smaller value is used + function test_liquidateTrove_maxDebtRespectedBelowSafeCRCap( + uint256 _amount + ) public { + _amount = bound(_amount, troveManager.min_debt() * 4, maxFuzzAmount); + + mintAndDepositIntoLender(userLender, _amount); + + uint256 _collateralNeeded = + (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + + ITroveManager.Trove memory _trove = troveManager.troves(_troveId); + + // Drop price to 1% below MCR + uint256 _price; + if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { + _price = + troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); + } else { + _price = + troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; + } + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_price)); + vm.mockCall( + address(priceOracle), + abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), + abi.encode(_price * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION)) + ); + + // Liquidate a small amount (1/4 of debt) -- less than what safe CR would allow + uint256 _smallDebt = _trove.debt / 4; + uint256 _crBefore = (_trove.collateral * _price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; + uint256 _expectedCollateral = calculateCollateralToDecrease(_crBefore, _smallDebt, _price, _trove.collateral); + + liquidatorMock.liquidate(_troveId, _smallDebt); + + // Trove should be active with exactly the requested debt removed + ITroveManager.Trove memory _troveAfter = troveManager.troves(_troveId); + assertEq(uint256(_troveAfter.status), uint256(ITroveManager.Status.active), "E0"); + assertEq(_troveAfter.debt, _trove.debt - _smallDebt, "E1"); + assertEq(_troveAfter.collateral, _trove.collateral - _expectedCollateral, "E2"); + + // CR should have improved + uint256 _crAfter = (_troveAfter.collateral * _price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _troveAfter.debt; + assertGt(_crAfter, _crBefore, "E3"); + + // CR should be below safe CR (we didn't liquidate enough to reach it) + assertLt(_crAfter, troveManager.safe_collateral_ratio(), "E4"); + + // But still above MCR + assertGe(_crAfter, troveManager.minimum_collateral_ratio(), "E5"); + } + + // When safe CR cap + min_debt check forces full liquidation + function test_liquidateTrove_safeCRCapForcesFullViaMinDebt( + uint256 _amount + ) public { + _amount = bound(_amount, troveManager.min_debt(), troveManager.min_debt() * 13 / 10); + + mintAndDepositIntoLender(userLender, _amount); + + uint256 _collateralNeeded = + (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + + ITroveManager.Trove memory _trove = troveManager.troves(_troveId); + + // Drop price to 1% below MCR + uint256 _price; + if (BORROW_TOKEN_PRECISION < COLLATERAL_TOKEN_PRECISION) { + _price = + troveManager.minimum_collateral_ratio() * _trove.debt * ORACLE_PRICE_SCALE * 99 / (100 * _trove.collateral * BORROW_TOKEN_PRECISION); + } else { + _price = + troveManager.minimum_collateral_ratio() * _trove.debt / (100 * _trove.collateral) * ORACLE_PRICE_SCALE / BORROW_TOKEN_PRECISION * 99; + } + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_price)); + vm.mockCall( + address(priceOracle), + abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), + abi.encode(_price * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION)) + ); + + // Liquidate with max --> safe CR cap would leave remaining debt below min_debt --> forced full + liquidate(_troveId); + + // Trove should be fully liquidated + ITroveManager.Trove memory _troveAfter = troveManager.troves(_troveId); + assertEq(uint256(_troveAfter.status), uint256(ITroveManager.Status.liquidated), "E0"); + assertEq(_troveAfter.debt, 0, "E1"); + assertEq(_troveAfter.collateral, 0, "E2"); + + // Trove owner received remaining collateral + assertGt(collateralToken.balanceOf(userBorrower), 0, "E3"); + + // Trove removed from sorted list + assertFalse(sortedTroves.contains(_troveId), "E4"); + } + + // Bad debt scenario: CR < 100% + fee --> safe CR formula returns > total debt --> full liquidation + function test_liquidateTrove_badDebtFullLiquidation( + uint256 _amount + ) public { + _amount = bound(_amount, troveManager.min_debt(), maxFuzzAmount); + + mintAndDepositIntoLender(userLender, _amount); + + uint256 _collateralNeeded = + (_amount * DEFAULT_TARGET_COLLATERAL_RATIO / BORROW_TOKEN_PRECISION) * ORACLE_PRICE_SCALE / priceOracle.get_price(); + + uint256 _troveId = mintAndOpenTrove(userBorrower, _collateralNeeded, _amount, DEFAULT_ANNUAL_INTEREST_RATE); + + ITroveManager.Trove memory _trove = troveManager.troves(_troveId); + + // Drop price dramatically to put CR below 100% (bad debt) + // Target CR ≈ 98% + uint256 _price = 98 * troveManager.one_pct() * _trove.debt * ORACLE_PRICE_SCALE / (_trove.collateral * BORROW_TOKEN_PRECISION); + vm.mockCall(address(priceOracle), abi.encodeWithSelector(IPriceOracleScaled.get_price.selector), abi.encode(_price)); + vm.mockCall( + address(priceOracle), + abi.encodeWithSelector(IPriceOracleNotScaled.get_price.selector, false), + abi.encode(_price * COLLATERAL_TOKEN_PRECISION * WAD / (ORACLE_PRICE_SCALE * BORROW_TOKEN_PRECISION)) + ); + + // Verify CR is below 100% + uint256 _cr = (_trove.collateral * _price / ORACLE_PRICE_SCALE) * BORROW_TOKEN_PRECISION / _trove.debt; + assertLt(_cr, BORROW_TOKEN_PRECISION, "E0"); + + // Liquidate --> safe CR formula returns > total debt --> min() caps at total debt --> full liquidation + liquidate(_troveId); + + // Trove should be fully liquidated + ITroveManager.Trove memory _troveAfter = troveManager.troves(_troveId); + assertEq(uint256(_troveAfter.status), uint256(ITroveManager.Status.liquidated), "E1"); + assertEq(_troveAfter.debt, 0, "E2"); + assertEq(_troveAfter.collateral, 0, "E3"); + + // Liquidator gets all collateral (bad debt means collateral_with_fee > trove.collateral) + assertEq(collateralToken.balanceOf(address(liquidatorMock)), _trove.collateral, "E4"); + + // Trove owner gets nothing (all collateral went to liquidator) + assertEq(collateralToken.balanceOf(userBorrower), 0, "E5"); + + // Trove removed from sorted list + assertFalse(sortedTroves.contains(_troveId), "E6"); + } + } diff --git a/test/TroveManager.t.sol b/test/TroveManager.t.sol index 5f33159..02fd2f3 100644 --- a/test/TroveManager.t.sol +++ b/test/TroveManager.t.sol @@ -45,6 +45,7 @@ contract TroveManagerTests is Base { borrow_token: address(borrowToken), collateral_token: address(collateralToken), minimum_debt: minimumDebt, + safe_collateral_ratio: safeCollateralRatio, minimum_collateral_ratio: minimumCollateralRatio, max_penalty_collateral_ratio: maxPenaltyCollateralRatio, min_liquidation_fee: minLiquidationFee, diff --git a/test/interfaces/ITroveManager.sol b/test/interfaces/ITroveManager.sol index 5f7f009..ca45782 100644 --- a/test/interfaces/ITroveManager.sol +++ b/test/interfaces/ITroveManager.sol @@ -43,6 +43,7 @@ interface ITroveManager { address borrow_token; address collateral_token; uint256 minimum_debt; + uint256 safe_collateral_ratio; uint256 minimum_collateral_ratio; uint256 max_penalty_collateral_ratio; uint256 min_liquidation_fee; @@ -69,6 +70,7 @@ interface ITroveManager { function one_pct() external view returns (uint256); function borrow_token_precision() external view returns (uint256); function min_debt() external view returns (uint256); + function safe_collateral_ratio() external view returns (uint256); function minimum_collateral_ratio() external view returns (uint256); function max_penalty_collateral_ratio() external view returns (uint256); function min_liquidation_fee() external view returns (uint256); @@ -190,7 +192,7 @@ interface ITroveManager { function liquidate_trove( uint256 trove_id, - uint256 max_amount, + uint256 max_debt_to_repay, address receiver, bytes calldata data ) external returns (uint256); From a57092852916551aacf195e850158951d9bdfcb8 Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Mon, 16 Feb 2026 21:03:40 -0300 Subject: [PATCH 7/9] chore: unrelated but small docs fix --- src/oracles/yvcrvusd2_to_usdc_oracle.vy | 2 +- src/oracles/yvweth2_to_usdc_oracle.vy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/oracles/yvcrvusd2_to_usdc_oracle.vy b/src/oracles/yvcrvusd2_to_usdc_oracle.vy index 7921fd6..8039c82 100644 --- a/src/oracles/yvcrvusd2_to_usdc_oracle.vy +++ b/src/oracles/yvcrvusd2_to_usdc_oracle.vy @@ -98,7 +98,7 @@ def get_price(scaled: bool = True) -> uint256: assert usdc_usd_price > 0, "wtf" # Fetch yvcrvUSD-2 price per share - pps: uint256 = staticcall IYearnVault(_COLLATERAL_TOKEN.address).pricePerShare() # yvcrvUSD-2 in USD WAD + pps: uint256 = staticcall IYearnVault(_COLLATERAL_TOKEN.address).pricePerShare() # yvcrvUSD-2 in crvUSD # Calculate yvcrvUSD-2/USDC price price: uint256 = convert(crvusd_usd_price, uint256) * _WAD // convert(usdc_usd_price, uint256) * pps // _WAD # yvcrvUSD-2 in USDC WAD diff --git a/src/oracles/yvweth2_to_usdc_oracle.vy b/src/oracles/yvweth2_to_usdc_oracle.vy index 7a6f57c..cdb600c 100644 --- a/src/oracles/yvweth2_to_usdc_oracle.vy +++ b/src/oracles/yvweth2_to_usdc_oracle.vy @@ -98,7 +98,7 @@ def get_price(scaled: bool = True) -> uint256: assert usdc_usd_price > 0, "wtf" # Fetch yvWETH-2 price per share - pps: uint256 = staticcall IYearnVault(_COLLATERAL_TOKEN.address).pricePerShare() # yvWETH-2 in WETH WAD + pps: uint256 = staticcall IYearnVault(_COLLATERAL_TOKEN.address).pricePerShare() # yvWETH-2 in WETH # Calculate yvWETH-2/USDC price price: uint256 = convert(eth_usd_price, uint256) * _WAD // convert(usdc_usd_price, uint256) * pps // _WAD # yvWETH-2 in USDC WAD From ec79fe50217f3d7c0ce88dd3399eef3d39e083a8 Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Mon, 16 Feb 2026 21:04:18 -0300 Subject: [PATCH 8/9] chore: unrelated but small docs fix --- src/oracles/yvcrvusd2_to_usdc_oracle.vy | 4 ++-- src/oracles/yvweth2_to_usdc_oracle.vy | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/oracles/yvcrvusd2_to_usdc_oracle.vy b/src/oracles/yvcrvusd2_to_usdc_oracle.vy index 8039c82..d3ce90a 100644 --- a/src/oracles/yvcrvusd2_to_usdc_oracle.vy +++ b/src/oracles/yvcrvusd2_to_usdc_oracle.vy @@ -98,10 +98,10 @@ def get_price(scaled: bool = True) -> uint256: assert usdc_usd_price > 0, "wtf" # Fetch yvcrvUSD-2 price per share - pps: uint256 = staticcall IYearnVault(_COLLATERAL_TOKEN.address).pricePerShare() # yvcrvUSD-2 in crvUSD + pps: uint256 = staticcall IYearnVault(_COLLATERAL_TOKEN.address).pricePerShare() # Calculate yvcrvUSD-2/USDC price - price: uint256 = convert(crvusd_usd_price, uint256) * _WAD // convert(usdc_usd_price, uint256) * pps // _WAD # yvcrvUSD-2 in USDC WAD + price: uint256 = convert(crvusd_usd_price, uint256) * _WAD // convert(usdc_usd_price, uint256) * pps // _WAD # Scale price to the required format if needed and return return price * _ORACLE_SCALE_FACTOR // _WAD if scaled else price \ No newline at end of file diff --git a/src/oracles/yvweth2_to_usdc_oracle.vy b/src/oracles/yvweth2_to_usdc_oracle.vy index cdb600c..5a9aa96 100644 --- a/src/oracles/yvweth2_to_usdc_oracle.vy +++ b/src/oracles/yvweth2_to_usdc_oracle.vy @@ -98,10 +98,10 @@ def get_price(scaled: bool = True) -> uint256: assert usdc_usd_price > 0, "wtf" # Fetch yvWETH-2 price per share - pps: uint256 = staticcall IYearnVault(_COLLATERAL_TOKEN.address).pricePerShare() # yvWETH-2 in WETH + pps: uint256 = staticcall IYearnVault(_COLLATERAL_TOKEN.address).pricePerShare() # Calculate yvWETH-2/USDC price - price: uint256 = convert(eth_usd_price, uint256) * _WAD // convert(usdc_usd_price, uint256) * pps // _WAD # yvWETH-2 in USDC WAD + price: uint256 = convert(eth_usd_price, uint256) * _WAD // convert(usdc_usd_price, uint256) * pps // _WAD # Scale price to the required format if needed and return return price * _ORACLE_SCALE_FACTOR // _WAD if scaled else price \ No newline at end of file From 6c721de88ba9ed1d2154704e1472649cc178335b Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Tue, 17 Feb 2026 12:35:52 -0300 Subject: [PATCH 9/9] chore: smol docs --- src/trove_manager.vy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/trove_manager.vy b/src/trove_manager.vy index 19f34a2..be54161 100644 --- a/src/trove_manager.vy +++ b/src/trove_manager.vy @@ -1065,7 +1065,7 @@ def liquidate_trove( # Determine how much collateral to liquidate, cap at the Trove's collateral collateral_to_liquidate: uint256 = min(collateral_with_fee, trove.collateral) - # Cache the trove owner before the trove is potentially emptied + # Cache the Trove owner before the Trove is potentially emptied trove_owner: address = trove.owner # Check if this is a full liquidation and cache the result @@ -1147,7 +1147,8 @@ def liquidate_trove( # Transfer the collateral tokens to the `receiver` assert extcall self.collateral_token.transfer(receiver, collateral_to_liquidate, default_return_value=True) - # If the caller provided data, perform the callback + # If the caller provided data, perform the callback. + # No reentrancy concern: all state updates are complete before the external call (CEI) if len(data) != 0: extcall ITaker(receiver).takeCallback( trove_id,