MEDIUM severity: Unredeemable Troves Can Pay Minimum Interest Rate. Troves with debt below MIN_DEBT are marked as unredeemable (zombie troves) and excluded from redemption lists, removing the risk of redemption and thus the incentive to pay higher interest rates. An attacker can create many such troves to pay only the minimum interest, gaining an unfair economic adv...

Vulnerable code:
// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.18;

import "./Interfaces/ITroveManager.sol";
import "./Interfaces/IAddressesRegistry.sol";
import "./Interfaces/IStabilityPool.sol";
import "./Interfaces/ICollSurplusPool.sol";
import "./Interfaces/IBoldToken.sol";
import "./Interfaces/ISortedTroves.sol";
import "./Interfaces/ITroveEvents.sol";
import "./Interfaces/ITroveNFT.sol";
import "./Interfaces/ICollateralRegistry.sol";
import "./Interfaces/IWETH.sol";
import "./Dependencies/LiquityBase.sol";

// import "forge-std/console2.sol";

contract TroveManager is LiquityBase, ITroveManager, ITroveEvents {
    // --- Connected contract declarations ---

    ITroveNFT public troveNFT;
    IBorrowerOperations public borrowerOperations;
    IStabilityPool public stabilityPool;
    address internal gasPoolAddress;
    ICollSurplusPool internal collSurplusPool;
    IBoldToken internal boldToken;
    // A doubly linked list of Troves, sorted by their sorted by their collateral ratios
    ISortedTroves public sortedTroves;
    ICollateralRegistry internal collateralRegistry;
    // Wrapped ETH for liquidation reserve (gas compensation)
    IWETH internal immutable WETH;

    // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied
    uint256 public immutable CCR;

    // Minimum collateral ratio for individual troves
    uint256 internal immutable MCR;
    // Shutdown system collateral ratio. If the system's total collateral ratio (TCR) for a given collateral falls below the SCR,
    // the protocol triggers the shutdown of the borrow market and permanently disables all borrowing operations except for closing Troves.
    uint256 internal immutable SCR;

    // Liquidation penalty for troves offset to the SP
    uint256 internal immutable LIQUIDATION_PENALTY_SP;
    // Liquidation penalty for troves redistributed
    uint256 internal immutable LIQUIDATION_PENALTY_REDISTRIBUTION;

    // --- Data structures ---

    // Store the necessary data for a trove
    struct Trove {
        uint256 debt;
        uint256 coll;
        uint256 stake;
        Status status;
        uint64 arrayIndex;
        uint64 lastDebtUpdateTime;
        uint64 lastInterestRateAdjTime;
        uint256 annualInterestRate;
        address interestBatchManager;
        uint256 batchDebtShares;
    }

    mapping(uint256 => Trove) public Troves;

    // Store the necessary data for an interest batch manager. We treat each batch as a “big trove”.
    // Each trove has a share of the debt and a share of the coll of the global batch (will in general be different, as CRs are different).
    struct Batch {
        uint256 debt;
        uint256 coll;
        uint64 arrayIndex;
        uint64 lastDebtUpdateTime;
        uint64 lastInterestRateAdjTime;
        uint256 annualInterestRate;
        uint256 annualManagementFee;
        uint256 totalDebtShares;
    }

    mapping(address => Batch) internal batches;

    uint256 internal totalStakes;

    // Snapshot of the value of totalStakes, taken immediately after the latest liquidation
    uint256 internal totalStakesSnapshot;

    // Snapshot of the total collateral across the ActivePool and DefaultPool, immediately after the latest liquidation.
    uint256 internal totalCollateralSnapshot;

    /*
    * L_coll and L_boldDebt track the sums of accumulated liquidation rewards per unit staked. During its lifetime, each stake earns:
    *
    * An Coll gain of ( stake * [L_coll - L_coll(0)] )
    * A boldDebt increase  of ( stake * [L_boldDebt - L_boldDebt(0)] )
    *
    * Where L_coll(0) and L_boldDebt(0) are snapshots of L_coll and L_boldDebt for the active Trove taken at the instant the stake was made
    */
    uint256 internal L_coll;
    uint256 internal L_boldDebt;

    // Map active troves to their RewardSnapshot
    mapping(uint256 => RewardSnapshot) public rewardSnapshots;

    // Object containing the Coll and Bold snapshots for a given active trove
    struct RewardSnapshot {
        uint256 coll;
        uint256 boldDebt;
    }

    // Array of all active trove addresses - used to compute an approximate hint off-chain, for the sorted list insertion
    uint256[] internal TroveIds;
    // Array of all batch managers - used to fetch them off-chain
    address[] public batchIds;

    // Error trackers for the trove redistribution calculation
    uint256 internal lastCollError_Redistribution;
    uint256 internal lastBoldDebtError_Redistribution;

    // Timestamp at which branch was shut down. 0 if not shut down.
    uint256 public shutdownTime;

    /*
    * --- Variable container structs for liquidations ---
    *
    * These structs are used to hold, return and assign variables inside the liquidation functions,
    * in order to avoid the error: "CompilerError: Stack too deep".
    **/

    struct LiquidationValues {
        uint256 collGasCompensation;
        uint256 debtToOffset;
        uint256 collToSendToSP;
        uint256 debtToRedistribute;
        uint256 collToRedistribute;
        uint256 collSurplus;
        uint256 ETHGasCompensation;
        uint256 oldWeightedRecordedDebt;
        uint256 newWeightedRecordedDebt;
    }

    // --- Variable container structs for redemptions ---

    struct SingleRedemptionValues {
        uint256 troveId;
        address batchAddress;
        uint256 boldLot;
        uint256 collLot;
        uint256 collFee;
        uint256 appliedRedistBoldDebtGain;
        uint256 oldWeightedRecordedDebt;
        uint256 newWeightedRecordedDebt;
        uint256 newStake;
        LatestTroveData trove;
        LatestBatchData batch;
    }

    // --- Errors ---

    error EmptyData();
    error NothingToLiquidate();
    error CallerNotBorrowerOperations();
    error CallerNotCollateralRegistry();
    error OnlyOneTroveLeft();
    error NotShutDown();
    error NotEnoughBoldBalance();
    error MinCollNotReached(uint256 _coll);

    // --- Events ---

    event TroveNFTAddressChanged(address _newTroveNFTAddress);
    event BorrowerOperationsAddressChanged(address _newBorrowerOperationsAddress);
    event BoldTokenAddressChanged(address _newBoldTokenAddress);
    event StabilityPoolAddressChanged(address _stabilityPoolAddress);
    event GasPoolAddressChanged(address _gasPoolAddress);
    event CollSurplusPoolAddressChanged(address _collSurplusPoolAddress);
    event SortedTrovesAddressChanged(address _sortedTrovesAddress);
    event CollateralRegistryAddressChanged(address _collateralRegistryAddress);

    constructor(IAddressesRegistry _addressesRegistry) LiquityBase(_addressesRegistry) {
        CCR = _addressesRegistry.CCR();
        MCR = _addressesRegistry.MCR();
        SCR = _addressesRegistry.SCR();
        LIQUIDATION_PENALTY_SP = _addressesRegistry.LIQUIDATION_PENALTY_SP();
        LIQUIDATION_PENALTY_REDISTRIBUTION = _addressesRegistry.LIQUIDATION_PENALTY_REDISTRIBUTION();

        troveNFT = _addressesRegistry.troveNFT();
        borrowerOperations = _addressesRegistry.borrowerOperations();
        stabilityPool = _addressesRegistry.stabilityPool();
        gasPoolAddress = _addressesRegistry.gasPoolAddress();
        collSurplusPool = _addressesRegistry.collSurplusPool();
        boldToken = _addressesRegistry.boldToken();
        sortedTroves = _addressesRegistry.sortedTroves();
        WETH = _addressesRegistry.WETH();
        collateralRegistry = _addressesRegistry.collateralRegistry();

        emit TroveNFTAddressChanged(address(troveNFT));
        emit BorrowerOperationsAddressChanged(address(borrowerOperations));
        emit StabilityPoolAddressChanged(address(stabilityPool));
        emit GasPoolAddressChanged(gasPoolAddress);
        emit CollSurplusPoolAddressChanged(address(collSurplusPool));
        emit BoldTokenAddressChanged(address(boldToken));
        emit SortedTrovesAddressChanged(address(sortedTroves));
        emit CollateralRegistryAddressChanged(address(collateralRegistry));
    }

    // --- Getters ---

    function getTroveIdsCount() external view override returns (uint256) {
        return TroveIds.length;
    }

    function getTroveFromTroveIdsArray(uint256 _index) external view override returns (uint256) {
        return TroveIds[_index];
    }

    // --- Trove Liquidation functions ---

    // --- Inner single liquidation functions ---

    // Liquidate one trove
    function _liquidate(
        IDefaultPool _defaultPool,
        uint256 _troveId,
        uint256 _boldInStabPool,
        uint256 _price,
        LatestTroveData memory trove,
        LiquidationValues memory singleLiquidation
    ) internal {
        address owner = troveNFT.ownerOf(_troveId);

        _getLatestTroveData(_troveId, trove);
        address batchAddress = _getBatchManager(_troveId);
        bool isTroveInBatch = batchAddress != address(0);
        LatestBatchData memory batch;
        if (isTroveInBatch) _getLatestBatchData(batchAddress, batch);

        _movePendingTroveRewardsToActivePool(_defaultPool, trove.redistBoldDebtGain, trove.redistCollGain);

        singleLiquidation.collGasCompensation = _getCollGasCompensation(trove.entireColl);
        uint256 collToLiquidate = trove.entireColl - singleLiquidation.collGasCompensation;

        (
            singleLiquidation.debtToOffset,
            singleLiquidation.collToSendToSP,
            singleLiquidation.debtToRedistribute,
            singleLiquidation.collToRedistribute,
            singleLiquidation.collSurplus
        ) = _getOffsetAndRedistributionVals(trove.entireDebt, collToLiquidate, _boldInStabPool, _price);

        TroveChange memory troveChange;
        troveChange.collDecrease = trove.entireColl;
        troveChange.debtDecrease = trove.entireDebt;
        troveChange.appliedRedistCollGain = trove.redistCollGain;
        troveChange.appliedRedistBoldDebtGain = trove.redistBoldDebtGain;
        _closeTrove(
            _troveId,
            troveChange,
            batchAddress,
            batch.entireCollWithoutRedistribution,
            batch.entireDebtWithoutRedistribution,
            Status.closedByLiquidation
        );

        if (isTroveInBatch) {
            singleLiquidation.oldWeightedRecordedDebt =
                batch.weightedRecordedDebt + (trove.entireDebt - trove.redistBoldDebtGain) * batch.annualInterestRate;
            singleLiquidation.newWeightedRecordedDebt = batch.entireDebtWithoutRedistribution * batch.annualInterestRate;
            // Mint batch management fee
            troveChange.batchAccruedManagementFee = batch.accruedManagementFee;
            troveChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee
                + (trove.entireDebt - trove.redistBoldDebtGain) * batch.annualManagementFee;
            troveChange.newWeightedRecordedBatchManagementFee =
                batch.entireDebtWithoutRedistribution * batch.annualManagementFee;
            activePool.mintBatchManagementFeeAndAccountForChange(troveChange, batchAddress);
        } else {
            singleLiquidation.oldWeightedRecordedDebt = trove.weightedRecordedDebt;
        }

        // Differencen between liquidation penalty and liquidation threshold
        if (singleLiquidation.collSurplus > 0) {
            collSurplusPool.accountSurplus(owner, singleLiquidation.collSurplus);
        }

        // Wipe out state in BO
        borrowerOperations.onLiquidateTrove(_troveId);

        emit TroveUpdated({
            _troveId: _troveId,
            _debt: 0,
            _coll: 0,
            _stake: 0,
            _annualInterestRate: 0,
            _snapshotOfTotalCollRedist: 0,
            _snapshotOfTotalDebtRedist: 0
        });

        emit TroveOperation({
            _troveId: _troveId,
            _operation: Operation.liquidate,
            _annualInterestRate: 0,
            _debtIncreaseFromRedist: trove.redistBoldDebtGain,
            _debtIncreaseFromUpfrontFee: 0,
            _debtChangeFromOperation: -int256(trove.entireDebt),
            _collIncreaseFromRedist: trove.redistCollGain,
            _collChangeFromOperation: -int256(trove.entireColl)
        });

        emit BatchUpdated({
            _interestBatchManager: batchAddress,
            _operation: BatchOperation.exitBatch,
            _debt: batches[batchAddress].debt,
            _coll: batches[batchAddress].coll,
            _annualInterestRate: batch.annualInterestRate,
            _annualManagementFee: batch.annualManagementFee,
            _totalDebtShares: batches[batchAddress].totalDebtShares
        });
    }

    // Return the amount of Coll to be drawn from a trove's collateral and sent as gas compensation.
    function _getCollGasCompensation(uint256 _entireColl) internal pure returns (uint256) {
        return LiquityMath._min(_entireColl / COLL_GAS_COMPENSATION_DIVISOR, COLL_GAS_COMPENSATION_CAP);
    }

    /* In a full liquidation, returns the values for a trove's coll and debt to be offset, and coll and debt to be
    * redistributed to active troves.
    */
    function _getOffsetAndRedistributionVals(
        uint256 _entireTroveDebt,
        uint256 _collToLiquidate, // gas compensation is already subtracted
        uint256 _boldInStabPool,
        uint256 _price
    )
        internal
        view
        returns (
            uint256 debtToOffset,
            uint256 collToSendToSP,
            uint256 debtToRedistribute,
            uint256 collToRedistribute,
            uint256 collSurplus
        )
    {
        uint256 collSPPortion;
        /*
         * Offset as much debt & collateral as possible against the Stability Pool, and redistribute the remainder
         * between all active troves.
         *
         *  If the trove's debt is larger than the deposited Bold in the Stability Pool:
         *
         *  - Offset an amount of the trove's debt equal to the Bold in the Stability Pool
         *  - Send a fraction of the trove's collateral to the Stability Pool, equal to the fraction of its offset debt
         *
         */
        if (_boldInStabPool > 0) {
            debtToOffset = LiquityMath._min(_entireTroveDebt, _boldInStabPool);
            collSPPortion = _collToLiquidate * debtToOffset / _entireTroveDebt;
            (collToSendToSP, collSurplus) =
                _getCollPenaltyAndSurplus(collSPPortion, debtToOffset, LIQUIDATION_PENALTY_SP, _price);
        }

        // Redistribution
        debtToRedistribute = _entireTroveDebt - debtToOffset;
        if (debtToRedistribute > 0) {
            uint256 collRedistributionPortion = _collToLiquidate - collSPPortion;
            if (collRedistributionPortion > 0) {
                (collToRedistribute, collSurplus) = _getCollPenaltyAndSurplus(
                    collRedistributionPortion + collSurplus, // Coll surplus from offset can be eaten up by red. penalty
                    debtToRedistribute,
                    LIQUIDATION_PENALTY_REDISTRIBUTION, // _penaltyRatio
                    _price
                );
            }
        }
        // assert(_collToLiquidate == collToSendToSP + collToRedistribute + collSurplus);
    }

    function _getCollPenaltyAndSurplus(
        uint256 _collToLiquidate,
        uint256 _debtToLiquidate,
        uint256 _penaltyRatio,
        uint256 _price
    ) internal pure returns (uint256 seizedColl, uint256 collSurplus) {
        uint256 maxSeizedColl = _debtToLiquidate * (DECIMAL_PRECISION + _penaltyRatio) / _price;
        if (_collToLiquidate > maxSeizedColl) {
            seizedColl = maxSeizedColl;
            collSurplus = _collToLiquidate - maxSeizedColl;
        } else {
            seizedColl = _collToLiquidate;
            collSurplus = 0;
        }
    }

    /*
     * Attempt to liquidate a custom list of troves provided by the caller.
     */
    function batchLiquidateTroves(uint256[] memory _troveArray) public override {
        if (_troveArray.length == 0) {
            revert EmptyData();
        }

        IActivePool activePoolCached = activePool;
        IDefaultPool defaultPoolCached = defaultPool;
        IStabilityPool stabilityPoolCached = stabilityPool;

        TroveChange memory troveChange;
        LiquidationValues memory totals;

        (uint256 price, ) = priceFeed.fetchPrice();
        uint256 boldInStabPool = stabilityPoolCached.getTotalBoldDeposits();

        // Perform the appropriate liquidation sequence - tally values and obtain their totals.
        _batchLiquidateTroves(defaultPoolCached, price, boldInStabPool, _troveArray, totals, troveChange);

        if (troveChange.debtDecrease == 0) {
            revert NothingToLiquidate();
        }

        activePoolCached.mintAggInterestAndAccountForTroveChange(troveChange, address(0));

        // Move liquidated Coll and Bold to the appropriate pools
        if (totals.debtToOffset > 0 || totals.collToSendToSP > 0) {
            stabilityPoolCached.offset(totals.debtToOffset, totals.collToSendToSP);
        }
        // we check amount is not zero inside
        _redistributeDebtAndColl(
            activePoolCached, defaultPoolCached, totals.debtToRedistribute, totals.collToRedistribute
        );
        if (totals.collSurplus > 0) {
            activePoolCached.sendColl(address(collSurplusPool), totals.collSurplus);
        }

        // Update system snapshots
        _updateSystemSnapshots_excludeCollRemainder(activePoolCached, totals.collGasCompensation);

        emit Liquidation(
            totals.debtToOffset,
            totals.debtToRedistribute,
            totals.ETHGasCompensation,
            totals.collGasCompensation,
            totals.collToSendToSP,
            totals.collToRedistribute,
            totals.collSurplus,
            L_coll,
            L_boldDebt,
            price
        );

        // Send gas compensation to caller
        _sendGasCompensation(activePoolCached, msg.sender, totals.ETHGasCompensation, totals.collGasCompensation);
    }

    function _isLiquidatableStatus(Status _status) internal pure returns (bool) {
        return _status == Status.active || _status == Status.unredeemable;
    }

    function _batchLiquidateTroves(
        IDefaultPool _defaultPool,
        uint256 _price,
        uint256 _boldInStabPool,
        uint256[] memory _troveArray,
        LiquidationValues memory totals,
        TroveChange memory troveChange
    ) internal {
        uint256 remainingBoldInStabPool = _boldInStabPool;

        for (uint256 i = 0; i < _troveArray.length; i++) {
            uint256 troveId = _troveArray[i];

            // Skip non-liquidatable troves
            if (!_isLiquidatableStatus(Troves[troveId].status)) continue;

            uint256 ICR = getCurrentICR(troveId, _price);

            if (ICR < MCR) {
                LiquidationValues memory singleLiquidation;
                LatestTroveData memory trove;

                _liquidate(_defaultPool, troveId, remainingBoldInStabPool, _price, trove, singleLiquidation);
                remainingBoldInStabPool -= singleLiquidation.debtToOffset;

                // Add liquidation values to their respective running totals
                _addLiquidationValuesToTotals(trove, singleLiquidation, totals, troveChange);
            }
        }
    }

    // --- Liquidation helper functions ---

    // Adds all values from `singleLiquidation` to their respective totals in `totals` in-place
    function _addLiquidationValuesToTotals(
        LatestTroveData memory _trove,
        LiquidationValues memory _singleLiquidation,
        LiquidationValues memory totals,
        TroveChange memory troveChange
    ) internal pure {
        // Tally all the values with their respective running totals
        totals.collGasCompensation += _singleLiquidation.collGasCompensation;
        totals.ETHGasCompensation += ETH_GAS_COMPENSATION;
        troveChange.debtDecrease += _trove.entireDebt;
        troveChange.collDecrease += _trove.entireColl;
        troveChange.appliedRedistBoldDebtGain += _trove.redistBoldDebtGain;
        troveChange.oldWeightedRecordedDebt += _singleLiquidation.oldWeightedRecordedDebt;
        troveChange.newWeightedRecordedDebt += _singleLiquidation.newWeightedRecordedDebt;
        totals.debtToOffset += _singleLiquidation.debtToOffset;
        totals.collToSendToSP += _singleLiquidation.collToSendToSP;
        totals.debtToRedistribute += _singleLiquidation.debtToRedistribute;
        totals.collToRedistribute += _singleLiquidation.collToRedistribute;
        totals.collSurplus += _singleLiquidation.collSurplus;
    }

    function _sendGasCompensation(IActivePool _activePool, address _liquidator, uint256 _eth, uint256 _coll) internal {
        if (_eth > 0) {
            WETH.transferFrom(gasPoolAddress, _liquidator, _eth);
        }

        if (_coll > 0) {
            _activePool.sendColl(_liquidator, _coll);
        }
    }

    // Move a Trove's pending debt and collateral rewards from distributions, from the Default Pool to the Active Pool
    function _movePendingTroveRewardsToActivePool(IDefaultPool _defaultPool, uint256 _bold, uint256 _coll) internal {
        if (_bold > 0) {
            _defaultPool.decreaseBoldDebt(_bold);
        }

        if (_coll > 0) {
            _defaultPool.sendCollToActivePool(_coll);
        }
    }

    // --- Redemption functions ---

    function _applySingleRedemption(
        IDefaultPool _defaultPool,
        SingleRedemptionValues memory _singleRedemption,
        bool _isTroveInBatch
    ) internal returns (uint256) {
        // Decrease the debt and collateral of the current Trove according to the Bold lot and corresponding ETH to send
        uint256 newDebt = _singleRedemption.trove.entireDebt - _singleRedemption.boldLot;
        uint256 newColl = _singleRedemption.trove.entireColl - _singleRedemption.collLot;

        _singleRedemption.appliedRedistBoldDebtGain = _singleRedemption.trove.redistBoldDebtGain;

        if (_isTroveInBatch) {
            _getLatestBatchData(_singleRedemption.batchAddress, _singleRedemption.batch);
            // We know boldLot <= trove entire debt, so this subtraction is safe
            uint256 newAmountForWeightedDebt = _singleRedemption.batch.entireDebtWithoutRedistribution
                + _singleRedemption.trove.redistBoldDebtGain - _singleRedemption.boldLot;
            _singleRedemption.oldWeightedRecordedDebt = _singleRedemption.batch.weightedRecordedDebt;
            _singleRedemption.newWeightedRecordedDebt =
                newAmountForWeightedDebt * _singleRedemption.batch.annualInterestRate;

            TroveChange memory troveChange;
            troveChange.debtDecrease = _singleRedemption.boldLot;
            troveChange.collDecrease = _singleRedemption.collLot;
            troveChange.appliedRedistBoldDebtGain = _singleRedemption.trove.redistBoldDebtGain;
            troveChange.appliedRedistCollGain = _singleRedemption.trove.redistCollGain;
            // batchAccruedManagementFee is handled in the outer function
            troveChange.oldWeightedRecordedBatchManagementFee =
                _singleRedemption.batch.weightedRecordedBatchManagementFee;
            troveChange.newWeightedRecordedBatchManagementFee =
                newAmountForWeightedDebt * _singleRedemption.batch.annualManagementFee;

            activePool.mintBatchManagementFeeAndAccountForChange(troveChange, _singleRedemption.batchAddress);

            Troves[_singleRedemption.troveId].coll = newColl;
            // interest and fee were updated in the outer function
            _updateBatchShares(
                _singleRedemption.troveId,
                _singleRedemption.batchAddress,
                troveChange,
                _singleRedemption.batch.entireCollWithoutRedistribution,
                _singleRedemption.batch.entireDebtWithoutRedistribution
            );
        } else {
            _singleRedemption.oldWeightedRecordedDebt = _singleRedemption.trove.weightedRecordedDebt;
            _singleRedemption.newWeightedRecordedDebt = newDebt * _singleRedemption.trove.annualInterestRate;
            Troves[_singleRedemption.troveId].debt = newDebt;
            Troves[_singleRedemption.troveId].coll = newColl;
            Troves[_singleRedemption.troveId].lastDebtUpdateTime = uint64(block.timestamp);
        }

        _singleRedemption.newStake = _updateStakeAndTotalStakes(_singleRedemption.troveId, newColl);
        _movePendingTroveRewardsToActivePool(
            _defaultPool, _singleRedemption.trove.redistBoldDebtGain, _singleRedemption.trove.redistCollGain
        );
        _updateTroveRewardSnapshots(_singleRedemption.troveId);

        if (_isTroveInBatch) {
            emit BatchedTroveUpdated({
                _troveId: _singleRedemption.troveId,
                _interestBatchManager: _singleRedemption.batchAddress,
                _batchDebtShares: Troves[_singleRedemption.troveId].batchDebtShares,
                _coll: newColl,
                _stake: _singleRedemption.newStake,
                _snapshotOfTotalCollRedist: L_coll,
                _snapshotOfTotalDebtRedist: L_boldDebt
            });
        } else {
            emit TroveUpdated({
                _troveId: _singleRedemption.troveId,
                _debt: newDebt,
                _coll: newColl,
                _stake: _singleRedemption.newStake,
                _annualInterestRate: _singleRedemption.trove.annualInterestRate,
                _snapshotOfTotalCollRedist: L_coll,
                _snapshotOfTotalDebtRedist: L_boldDebt
            });
        }

        emit TroveOperation({
            _troveId: _singleRedemption.troveId,
            _operation: Operation.redeemCollateral,
            _annualInterestRate: _singleRedemption.trove.annualInterestRate,
            _debtIncreaseFromRedist: _singleRedemption.trove.redistBoldDebtGain,
            _debtIncreaseFromUpfrontFee: 0,
            _debtChangeFromOperation: -int256(_singleRedemption.boldLot),
            _collIncreaseFromRedist: _singleRedemption.trove.redistCollGain,
            _collChangeFromOperation: -int256(_singleRedemption.collLot)
        });

        emit RedemptionFeePaidToTrove(_singleRedemption.troveId, _singleRedemption.collFee);

        return newDebt;
    }

    // Redeem as much collateral as possible from _borrower's Trove in exchange for Bold up to _maxBoldamount
    function _redeemCollateralFromTrove(
        IDefaultPool _defaultPool,
        SingleRedemptionValues memory _singleRedemption,
        uint256 _maxBoldamount,
        uint256 _price,
        uint256 _redemptionRate
    ) internal {
        _getLatestTroveData(_singleRedemption.troveId, _singleRedemption.trove);

        // Determine the remaining amount (lot) to be redeemed, capped by the entire debt of the Trove
        _singleRedemption.boldLot = LiquityMath._min(_maxBoldamount, _singleRedemption.trove.entireDebt);

        // Get the amount of Coll equal in USD value to the boldLot redeemed
        uint256 correspondingColl = _singleRedemption.boldLot * DECIMAL_PRECISION / _price;
        // Calculate the collFee separately (for events)
        _singleRedemption.collFee = correspondingColl * _redemptionRate / DECIMAL_PRECISION;
        // Get the final collLot to send to redeemer, leaving the fee in the Trove
        _singleRedemption.collLot = correspondingColl - _singleRedemption.collFee;

        bool isTroveInBatch = _singleRedemption.batchAddress != address(0);
        uint256 newDebt = _applySingleRedemption(_defaultPool, _singleRedemption, isTroveInBatch);

        // Make Trove unredeemable if it's tiny, in order to prevent griefing future (normal, sequential) redemptions
        if (newDebt < MIN_DEBT) {
            Troves[_singleRedemption.troveId].status = Status.unredeemable;
            if (isTroveInBatch) {
                sortedTroves.removeFromBatch(_singleRedemption.troveId);
            } else {
                sortedTroves.remove(_singleRedemption.troveId);
            }
        }
    }

    function _updateBatchInterestPriorToRedemption(IActivePool _activePool, address _batchAddress) internal {
        LatestBatchData memory batch;
        _getLatestBatchData(_batchAddress, batch);
        batches[_batchAddress].debt = batch.entireDebtWithoutRedistribution;
        batches[_batchAddress].lastDebtUpdateTime = uint64(block.timestamp);
        // As we are updating the batch, we update the ActivePool weighted sum too
        TroveChange memory batchTroveChange;
        batchTroveChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt;
        batchTroveChange.newWeightedRecordedDebt = batch.entireDebtWithoutRedistribution * batch.annualInterestRate;
        batchTroveChange.batchAccruedManagementFee = batch.accruedManagementFee;
        batchTroveChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee;
        batchTroveChange.newWeightedRecordedBatchManagementFee =
            batch.entireDebtWithoutRedistribution * batch.annualManagementFee;

        _activePool.mintAggInterestAndAccountForTroveChange(batchTroveChange, _batchAddress);

        emit BatchUpdated({
            _interestBatchManager: _batchAddress,
            _operation: BatchOperation.troveChange,
            _debt: batch.entireDebtWithoutRedistribution,
            _coll: batch.entireCollWithoutRedistribution,
            _annualInterestRate: batch.annualInterestRate,
            _annualManagementFee: batch.annualManagementFee,
            _totalDebtShares: batches[_batchAddress].totalDebtShares
        });
    }

    /* Send _boldamount Bold to the system and redeem the corresponding amount of collateral from as many Troves as are needed to fill the redemption
    * request.  Applies redistribution gains to a Trove before reducing its debt and coll.
    *
    * Note that if _amount is very large, this function can run out of gas, specially if traversed troves are small. This can be easily avoided by
    * splitting the total _amount in appropriate chunks and calling the function multiple times.
    *
    * Param `_maxIterations` can also be provided, so the loop through Troves is capped (if it’s zero, it will be ignored).This makes it easier to
    * avoid OOG for the frontend, as only knowing approximately the average cost of an iteration is enough, without needing to know the “topology”
    * of the trove list. It also avoids the need to set the cap in stone in the contract, nor doing gas calculations, as both gas price and opcode
    * costs can vary.
    *
    * All Troves that are redeemed from -- with the likely exception of the last one -- will end up with no debt left, therefore they will be closed.
    * If the last Trove does have some remaining debt, it has a finite ICR, and the reinsertion could be anywhere in the list, therefore it requires a hint.
    * A frontend should use getRedemptionHints() to calculate what the ICR of this Trove will be after redemption, and pass a hint for its position
    * in the sortedTroves list along with the ICR value that the hint was found for.
    *
    * If another transaction modifies the list between calling getRedemptionHints() and passing the hints to redeemCollateral(), it
    * is very likely that the last (partially) redeemed Trove would end up with a different ICR than what the hint is for. In this case the
    * redemption will stop after the last completely redeemed Trove and the sender will keep the remaining Bold amount, which they can attempt
    * to redeem later.
    */
    function redeemCollateral(
        address _redeemer,
        uint256 _boldamount,
        uint256 _price,
        uint256 _redemptionRate,
        uint256 _maxIterations
    ) external override returns (uint256 _redemeedAmount) {
        _requireCallerIsCollateralRegistry();

        IActivePool activePoolCached = activePool;
        ISortedTroves sortedTrovesCached = sortedTroves;

        TroveChange memory totalsTroveChange;
        uint256 totalCollFee;

        uint256 remainingBold = _boldamount;

        SingleRedemptionValues memory singleRedemption;
        singleRedemption.troveId = sortedTrovesCached.getLast();
        address lastBatchUpdatedInterest = address(0);

        // Loop through the Troves starting from the one with lowest collateral ratio until _amount of Bold is exchanged for collateral
        if (_maxIterations == 0) _maxIterations = type(uint256).max;
        while (singleRedemption.troveId != 0 && remainingBold > 0 && _maxIterations > 0) {
            _maxIterations--;
            // Save the uint256 of the Trove preceding the current one
            uint256 nextUserToCheck = sortedTrovesCached.getPrev(singleRedemption.troveId);
            // Skip if ICR < 100%, to make sure that redemptions always improve the CR of hit Troves
            if (getCurrentICR(singleRedemption.troveId, _price) < _100pct) {
                singleRedemption.troveId = nextUserToCheck;
                continue;
            }

            // If it’s in a batch, we need to update interest first
            // We do it here outside, to avoid repeating for each trove in the same batch
            singleRedemption.batchAddress = _getBatchManager(singleRedemption.troveId);
            if (
                singleRedemption.batchAddress != address(0) && singleRedemption.batchAddress != lastBatchUpdatedInterest
            ) {
                _updateBatchInterestPriorToRedemption(activePoolCached, singleRedemption.batchAddress);
                lastBatchUpdatedInterest = singleRedemption.batchAddress;
            }

            _redeemCollateralFromTrove(defaultPool, singleRedemption, remainingBold, _price, _redemptionRate);

            totalsTroveChange.collDecrease += singleRedemption.collLot;
            totalsTroveChange.debtDecrease += singleRedemption.boldLot;
            totalsTroveChange.appliedRedistBoldDebtGain += singleRedemption.appliedRedistBoldDebtGain;
            // For recorded and weighted recorded debt totals, we need to capture the increases and decreases,
            // since the net debt change for a given Trove could be positive or negative: redemptions decrease a Trove's recorded
            // (and weighted recorded) debt, but the accrued interest increases it.
            totalsTroveChange.newWeightedRecordedDebt += singleRedemption.newWeightedRecordedDebt;
            totalsTroveChange.oldWeightedRecordedDebt += singleRedemption.oldWeightedRecordedDebt;
            totalCollFee += singleRedemption.collFee;

            remainingBold -= singleRedemption.boldLot;
            singleRedemption.troveId = nextUserToCheck;
        }

        // We are removing this condition to prevent blocking redemptions
        //require(totals.totalCollDrawn > 0, "TroveManager: Unable to redeem any amount");

        emit Redemption(
            _boldamount, totalsTroveChange.debtDecrease, totalsTroveChange.collDecrease, totalCollFee, _price
        );

        activePoolCached.mintAggInterestAndAccountForTroveChange(totalsTroveChange, address(0));

        // Send the redeemed Coll to sender
        activePoolCached.sendColl(_redeemer, totalsTroveChange.collDecrease);
        // We’ll burn all the Bold together out in the CollateralRegistry, to save gas

        return totalsTroveChange.debtDecrease;
    }

    // Redeem as much collateral as possible from _borrower's Trove in exchange for Bold up to _maxBoldamount
    function _urgentRedeemCollateralFromTrove(
        IDefaultPool _defaultPool,
        uint256 _maxBoldamount,
        uint256 _price,
        SingleRedemptionValues memory _singleRedemption
    ) internal {
        _getLatestTroveData(_singleRedemption.troveId, _singleRedemption.trove);

        // Determine the remaining amount (lot) to be redeemed, capped by the entire debt of the Trove minus the liquidation reserve
        _singleRedemption.boldLot = LiquityMath._min(_maxBoldamount, _singleRedemption.trove.entireDebt);

        // Get the amount of ETH equal in USD value to the BOLD lot redeemed
        _singleRedemption.collLot = _singleRedemption.boldLot * (DECIMAL_PRECISION + URGENT_REDEMPTION_BONUS) / _price;
        // As here we can redeem when CR < 100%, we need to cap by collateral too
        if (_singleRedemption.collLot > _singleRedemption.trove.entireColl) {
            _singleRedemption.collLot = _singleRedemption.trove.entireColl;
            _singleRedemption.boldLot =
                _singleRedemption.trove.entireColl * _price / (DECIMAL_PRECISION + URGENT_REDEMPTION_BONUS);
        }

        bool isTroveInBatch = _singleRedemption.batchAddress != address(0);
        _applySingleRedemption(_defaultPool, _singleRedemption, isTroveInBatch);

        // No need to make this Trove unredeemable if it has tiny debt, since:
        // - This collateral branch has shut down and urgent redemptions are enabled
        // - Urgent redemptions aren't sequential, so they can't be griefed by tiny Troves.
    }

    function urgentRedemption(uint256 _boldAmount, uint256[] calldata _troveIds, uint256 _minCollateral) external {
        _requireIsShutDown();
        _requireBoldBalanceCoversRedemption(boldToken, msg.sender, _boldAmount);

        IActivePool activePoolCached = activePool;
        TroveChange memory totalsTroveChange;

        (uint256 price, ) = priceFeed.fetchPrice();

        uint256 remainingBold = _boldAmount;
        for (uint256 i = 0; i < _troveIds.length; i++) {
            SingleRedemptionValues memory singleRedemption;
            singleRedemption.troveId = _troveIds[i];

            // If it’s in a batch, we need to update interest first
            // As we don’t have them ordered now, we cannot avoid repeating for each trove in the same batch
            singleRedemption.batchAddress = _getBatchManager(singleRedemption.troveId);
            if (singleRedemption.batchAddress != address(0)) {
                _updateBatchInterestPriorToRedemption(activePoolCached, singleRedemption.batchAddress);
            }

            _urgentRedeemCollateralFromTrove(defaultPool, remainingBold, price, singleRedemption);

            totalsTroveChange.collDecrease += singleRedemption.collLot;
            totalsTroveChange.debtDecrease += singleRedemption.boldLot;
            totalsTroveChange.appliedRedistBoldDebtGain += singleRedemption.appliedRedistBoldDebtGain;
            // For recorded and weighted recorded debt totals, we need to capture the increases and decreases,
            // since the net debt change for a given Trove could be positive or negative: redemptions decrease a Trove's recorded
            // (and weighted recorded) debt, but the accrued interest increases it.
            totalsTroveChange.newWeightedRecordedDebt += singleRedemption.newWeightedRecordedDebt;
            totalsTroveChange.oldWeightedRecordedDebt += singleRedemption.oldWeightedRecordedDebt;

            remainingBold -= singleRedemption.boldLot;
            if (remainingBold == 0) break;
        }

        if (totalsTroveChange.collDecrease < _minCollateral) {
            revert MinCollNotReached(totalsTroveChange.collDecrease);
        }

        emit Redemption(_boldAmount, totalsTroveChange.debtDecrease, totalsTroveChange.collDecrease, 0, price);

        // Since this branch is shut down, this will mint 0 interest.
        // We call this only to update the aggregate debt and weighted debt trackers.
        activePoolCached.mintAggInterestAndAccountForTroveChange(totalsTroveChange, address(0));

        // Send the redeemed coll to caller
        activePoolCached.sendColl(msg.sender, totalsTroveChange.collDecrease);
        // Burn bold
        boldToken.burn(msg.sender, totalsTroveChange.debtDecrease);
    }

    function shutdown() external {
        _requireCallerIsBorrowerOperations();
        shutdownTime = block.timestamp;
        activePool.setShutdownFlag();
    }

    // --- Helper functions ---

    // Return the current collateral ratio (ICR) of a given Trove. Takes a trove's pending coll and debt rewards from redistributions into account.
    function getCurrentICR(uint256 _troveId, uint256 _price) public view override returns (uint256) {
        LatestTroveData memory trove;
        _getLatestTroveData(_troveId, trove);
        return LiquityMath._computeCR(trove.entireColl, trove.entireDebt, _price);
    }

    function _updateTroveRewardSnapshots(uint256 _troveId) internal {
        rewardSnapshots[_troveId].coll = L_coll;
        rewardSnapshots[_troveId].boldDebt = L_boldDebt;
    }

    // Return the Troves entire debt and coll, including redistribution gains from redistributions.
    function _getLatestTroveData(uint256 _troveId, LatestTroveData memory trove) internal view {
        // If trove belongs to a batch, we fetch the batch and apply its share to obtained values
        address batchAddress = _getBatchManager(_troveId);
        if (batchAddress != address(0)) {
            LatestBatchData memory batch;
            _getLatestBatchData(batchAddress, batch);
            _getLatestTroveDataFromBatch(_troveId, batchAddress, trove, batch);
            return;
        }

        uint256 stake = Troves[_troveId].stake;
        trove.redistBoldDebtGain = stake * (L_boldDebt - rewardSnapshots[_troveId].boldDebt) / DECIMAL_PRECISION;
        trove.redistCollGain = stake * (L_coll - rewardSnapshots[_troveId].coll) / DECIMAL_PRECISION;

        trove.recordedDebt = Troves[_troveId].debt;
        trove.annualInterestRate = Troves[_troveId].annualInterestRate;
        trove.weightedRecordedDebt = trove.recordedDebt * trove.annualInterestRate;

        uint256 period = _getInterestPeriod(Troves[_troveId].lastDebtUpdateTime);
        trove.accruedInterest = _calcInterest(trove.weightedRecordedDebt, period);

        trove.entireDebt = trove.recordedDebt + trove.redistBoldDebtGain + trove.accruedInterest;
        trove.entireColl = Troves[_troveId].coll + trove.redistCollGain;
        trove.lastInterestRateAdjTime = Troves[_troveId].lastInterestRateAdjTime;
    }

    function _getLatestTroveDataFromBatch(
        uint256 _troveId,
        address _batchAddress,
        LatestTroveData memory _latestTroveData,
        LatestBatchData memory _latestBatchData
    ) internal view {
        Trove memory trove = Troves[_troveId];
        Batch memory batch = batches[_batchAddress];
        uint256 batchDebtShares = trove.batchDebtShares;
        uint256 totalDebtShares = batch.totalDebtShares;

        uint256 stake = trove.stake;
        //uint256 batchRedistBoldDebtGain = stake * (L_boldDebt - rewardBatchSnapshots[_batchAddress].boldDebt) / DECIMAL_PRECISION;
        _latestTroveData.redistBoldDebtGain =
            stake * (L_boldDebt - rewardSnapshots[_troveId].boldDebt) / DECIMAL_PRECISION;
        _latestTroveData.redistCollGain = stake * (L_coll - rewardSnapshots[_troveId].coll) / DECIMAL_PRECISION;

        if (totalDebtShares > 0) {
            _latestTroveData.recordedDebt = _latestBatchData.recordedDebt * batchDebtShares / totalDebtShares;
            _latestTroveData.weightedRecordedDebt =
                _latestBatchData.weightedRecordedDebt * batchDebtShares / totalDebtShares;
            _latestTroveData.accruedInterest = _latestBatchData.accruedInterest * batchDebtShares / totalDebtShares;
            _latestTroveData.accruedBatchManagementFee =
                _latestBatchData.accruedManagementFee * batchDebtShares / totalDebtShares;
        }
        _latestTroveData.annualInterestRate = _latestBatchData.annualInterestRate;

        // We can’t do pro-rata batch entireDebt, because redist gains are proportional to coll, not to debt
        _latestTroveData.entireDebt = _latestTroveData.recordedDebt + _latestTroveData.redistBoldDebtGain
            + _latestTroveData.accruedInterest + _latestTroveData.accruedBatchManagementFee;
        _latestTroveData.entireColl = trove.coll + _latestTroveData.redistCollGain;
        _latestTroveData.lastInterestRateAdjTime =
            LiquityMath._max(_latestBatchData.lastInterestRateAdjTime, trove.lastInterestRateAdjTime);
    }

    function getLatestTroveData(uint256 _troveId) external view returns (LatestTroveData memory trove) {
        _getLatestTroveData(_troveId, trove);
    }

    function getTroveAnnualInterestRate(uint256 _troveId) external view returns (uint256) {
        Trove memory trove = Troves[_troveId];
        address batchAddress = _getBatchManager(trove);
        if (batchAddress != address(0)) {
            return batches[batchAddress].annualInterestRate;
        }
        return trove.annualInterestRate;
    }

    function _getBatchManager(uint256 _troveId) internal view returns (address) {
        return Troves[_troveId].interestBatchManager;
    }

    function _getBatchManager(Trove memory trove) internal pure returns (address) {
        return trove.interestBatchManager;
    }

    // Return the Batch entire debt and coll, including redistribution gains from redistributions.
    function _getLatestBatchData(address _batchAddress, LatestBatchData memory latestBatchData) internal view {
        Batch memory batch = batches[_batchAddress];

        latestBatchData.recordedDebt = batch.debt;
        latestBatchData.annualInterestRate = batch.annualInterestRate;
        latestBatchData.weightedRecordedDebt = latestBatchData.recordedDebt * latestBatchData.annualInterestRate;
        uint256 period = _getInterestPeriod(batch.lastDebtUpdateTime);
        latestBatchData.accruedInterest = _calcInterest(latestBatchData.weightedRecordedDebt, period);
        latestBatchData.annualManagementFee = batch.annualManagementFee;
        latestBatchData.weightedRecordedBatchManagementFee =
            latestBatchData.recordedDebt * latestBatchData.annualManagementFee;
        latestBatchData.accruedManagementFee =
            _calcInterest(latestBatchData.weightedRecordedBatchManagementFee, period);

        latestBatchData.entireDebtWithoutRedistribution =
            latestBatchData.recordedDebt + latestBatchData.accruedInterest + latestBatchData.accruedManagementFee;
        latestBatchData.entireCollWithoutRedistribution = batch.coll;
        latestBatchData.lastDebtUpdateTime = batch.lastDebtUpdateTime;
        latestBatchData.lastInterestRateAdjTime = batch.lastInterestRateAdjTime;
    }

    function getLatestBatchData(address _batchAddress) external view returns (LatestBatchData memory batch) {
        _getLatestBatchData(_batchAddress, batch);
    }

    // Update borrower's stake based on their latest collateral value
    function _updateStakeAndTotalStakes(uint256 _troveId, uint256 _coll) internal returns (uint256 newStake) {
        newStake = _computeNewStake(_coll);
        uint256 oldStake = Troves[_troveId].stake;
        Troves[_troveId].stake = newStake;

        totalStakes = totalStakes - oldStake + newStake;
    }

    // Calculate a new stake based on the snapshots of the totalStakes and totalCollateral taken at the last liquidation
    function _computeNewStake(uint256 _coll) internal view returns (uint256) {
        uint256 stake;
        if (totalCollateralSnapshot == 0) {
            stake = _coll;
        } else {
            /*
            * The following assert() holds true because:
            * - The system always contains >= 1 trove
            * - When we close or liquidate a trove, we redistribute the redistribution gains, so if all troves were closed/liquidated,
            * rewards would’ve been emptied and totalCollateralSnapshot would be zero too.
            */
            // assert(totalStakesSnapshot > 0);
            stake = _coll * totalStakesSnapshot / totalCollateralSnapshot;
        }
        return stake;
    }

    function _redistributeDebtAndColl(
        IActivePool _activePool,
        IDefaultPool _defaultPool,
        uint256 _debtToRedistribute,
        uint256 _collToRedistribute
    ) internal {
        if (_debtToRedistribute == 0) return;

        /*
        * Add distributed coll and debt rewards-per-unit-staked to the running totals. Division uses a "feedback"
        * error correction, to keep the cumulative error low in the running totals L_coll and L_boldDebt:
        *
        * 1) Form numerators which compensate for the floor division errors that occurred the last time this
        * function was called.
        * 2) Calculate "per-unit-staked" ratios.
        * 3) Multiply each ratio back by its denominator, to reveal the current floor division error.
        * 4) Store these errors for use in the next correction when this function is called.
        * 5) Note: static analysis tools complain about this "division before multiplication", however, it is intended.
        */
        uint256 collNumerator = _collToRedistribute * DECIMAL_PRECISION + lastCollError_Redistribution;
        uint256 boldDebtNumerator = _debtToRedistribute * DECIMAL_PRECISION + lastBoldDebtError_Redistribution;

        // Get the per-unit-staked terms
        uint256 collRewardPerUnitStaked = collNumerator / totalStakes;
        uint256 boldDebtRewardPerUnitStaked = boldDebtNumerator / totalStakes;

        lastCollError_Redistribution = collNumerator - collRewardPerUnitStaked * totalStakes;
        lastBoldDebtError_Redistribution = boldDebtNumerator - boldDebtRewardPerUnitStaked * totalStakes;

        // Add per-unit-staked terms to the running totals
        L_coll = L_coll + collRewardPerUnitStaked;
        L_boldDebt = L_boldDebt + boldDebtRewardPerUnitStaked;

        _defaultPool.increaseBoldDebt(_debtToRedistribute);
        _activePool.sendCollToDefaultPool(_collToRedistribute);
    }

    /*
    * Updates snapshots of system total stakes and total collateral, excluding a given collateral remainder from the calculation.
    * Used in a liquidation sequence.
    */
    function _updateSystemSnapshots_excludeCollRemainder(IActivePool _activePool, uint256 _collRemainder) internal {
        totalStakesSnapshot = totalStakes;

        uint256 activeColl = _activePool.getCollBalance();
        uint256 liquidatedColl = defaultPool.getCollBalance();
        totalCollateralSnapshot = activeColl - _collRemainder + liquidatedColl;
    }

    /*
    * Remove a Trove owner from the TroveIds array, not preserving array order. Removing owner 'B' does the following:
    * [A B C D E] => [A E C D], and updates E's Trove struct to point to its new array index.
    */
    function _removeTroveId(uint256 _troveId, uint256 TroveIdsArrayLength) internal {
        uint64 index = Troves[_troveId].arrayIndex;
        uint256 idxLast = TroveIdsArrayLength - 1;

        // assert(index <= idxLast);

        uint256 idToMove = TroveIds[idxLast];

        TroveIds[index] = idToMove;
        Troves[idToMove].arrayIndex = index;

        TroveIds.pop();
    }

    function getTroveStatus(uint256 _troveId) external view override returns (Status) {
        return Troves[_troveId].status;
    }

    // --- Interest rate calculations ---

    function _getInterestPeriod(uint256 _lastDebtUpdateTime) internal view returns (uint256) {
        if (shutdownTime == 0) {
            // If branch is not shut down, interest is earned up to now.
            return block.timestamp - _lastDebtUpdateTime;
        } else if (shutdownTime > 0 && _lastDebtUpdateTime < shutdownTime) {
            // If branch is shut down and the Trove was not updated since shut down, interest is earned up to the shutdown time.
            return shutdownTime - _lastDebtUpdateTime;
        } else {
            // if (shutdownTime > 0 && _lastDebtUpdateTime >= shutdownTime)
            // If branch is shut down and the Trove was updated after shutdown, no interest is earned since.
            return 0;
        }
    }

    // --- 'require' wrapper functions ---

    function _requireCallerIsBorrowerOperations() internal view {
        if (msg.sender != address(borrowerOperations)) {
            revert CallerNotBorrowerOperations();
        }
    }

    function _requireCallerIsCollateralRegistry() internal view {
        if (msg.sender != address(collateralRegistry)) {
            revert CallerNotCollateralRegistry();
        }
    }

    function _requireMoreThanOneTroveInSystem(uint256 TroveIdsArrayLength) internal pure {
        if (TroveIdsArrayLength == 1) {
            revert OnlyOneTroveLeft();
        }
    }

    function _requireIsShutDown() internal view {
        if (shutdownTime == 0) {
            revert NotShutDown();
        }
    }

    function _requireBoldBalanceCoversRedemption(IBoldToken _boldToken, address _redeemer, uint256 _amount)
        internal
        view
    {
        uint256 boldBalance = _boldToken.balanceOf(_redeemer);
        if (boldBalance < _amount) {
            revert NotEnoughBoldBalance();
        }
    }

    // --- Trove property getters ---

    function getUnbackedPortionPriceAndRedeemability() external returns (uint256, uint256, bool) {
        uint256 totalDebt = getEntireSystemDebt();
        uint256 spSize = stabilityPool.getTotalBoldDeposits();
        uint256 unbackedPortion = totalDebt > spSize ? totalDebt - spSize : 0;

        (uint256 price, ) = priceFeed.fetchPrice();
        // It's redeemable if the TCR is above the shutdown threshold, and branch has not been shut down
        bool redeemable = _getTCR(price) >= SCR && shutdownTime == 0;

        return (unbackedPortion, price, redeemable);
    }

    // --- Trove property setters, called by BorrowerOperations ---

    function onOpenTrove(address _owner, uint256 _troveId, TroveChange memory _troveChange, uint256 _annualInterestRate)
        external
    {
        _requireCallerIsBorrowerOperations();

        uint256 newStake = _computeNewStake(_troveChange.collIncrease);

        // Trove memory newTrove;
        Troves[_troveId].debt = _troveChange.debtIncrease + _troveChange.upfrontFee;
        Troves[_troveId].coll = _troveChange.collIncrease;
        Troves[_troveId].stake = newStake;
        Troves[_troveId].status = Status.active;
        Troves[_troveId].arrayIndex = uint64(TroveIds.length);
        Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp);
        Troves[_troveId].lastInterestRateAdjTime = uint64(block.timestamp);
        Troves[_troveId].annualInterestRate = _annualInterestRate;

        // Push the trove's id to the Trove list
        TroveIds.push(_troveId);

        uint256 newTotalStakes = totalStakes + newStake;
        totalStakes = newTotalStakes;

        // mint ERC721
        troveNFT.mint(_owner, _troveId);

        _updateTroveRewardSnapshots(_troveId);

        emit TroveUpdated({
            _troveId: _troveId,
            _debt: _troveChange.debtIncrease + _troveChange.upfrontFee,
            _coll: _troveChange.collIncrease,
            _stake: newStake,
            _annualInterestRate: _annualInterestRate,
            _snapshotOfTotalCollRedist: L_coll,
            _snapshotOfTotalDebtRedist: L_boldDebt
        });

        emit TroveOperation({
            _troveId: _troveId,
            _operation: Operation.openTrove,
            _annualInterestRate: _annualInterestRate,
            _debtIncreaseFromRedist: 0,
            _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee,
            _debtChangeFromOperation: int256(_troveChange.debtIncrease),
            _collIncreaseFromRedist: 0,
            _collChangeFromOperation: int256(_troveChange.collIncrease)
        });
    }

    function onOpenTroveAndJoinBatch(
        address _owner,
        uint256 _troveId,
        TroveChange memory _troveChange,
        address _batchAddress,
        uint256 _batchColl,
        uint256 _batchDebt
    ) external {
        _requireCallerIsBorrowerOperations();
        // assert(batchIds[batches[_batchAddress].arrayIndex] == _batchAddress);

        uint256 newStake = _computeNewStake(_troveChange.collIncrease);

        // Trove memory newTrove;
        Troves[_troveId].coll = _troveChange.collIncrease;
        Troves[_troveId].stake = newStake;
        Troves[_troveId].status = Status.active;
        Troves[_troveId].arrayIndex = uint64(TroveIds.length);
        Troves[_troveId].interestBatchManager = _batchAddress;
        Troves[_troveId].lastInterestRateAdjTime = uint64(block.timestamp);

        _updateTroveRewardSnapshots(_troveId);

        // Push the trove's id to the Trove list
        TroveIds.push(_troveId);

        _updateBatchShares(_troveId, _batchAddress, _troveChange, _batchColl, _batchDebt);

        uint256 newTotalStakes = totalStakes + newStake;
        totalStakes = newTotalStakes;

        // mint ERC721
        troveNFT.mint(_owner, _troveId);

        emit BatchedTroveUpdated({
            _troveId: _troveId,
            _interestBatchManager: _batchAddress,
            _batchDebtShares: Troves[_troveId].batchDebtShares,
            _coll: _troveChange.collIncrease,
            _stake: newStake,
            _snapshotOfTotalCollRedist: L_coll,
            _snapshotOfTotalDebtRedist: L_boldDebt
        });

        emit TroveOperation({
            _troveId: _troveId,
            _operation: Operation.openTroveAndJoinBatch,
            _annualInterestRate: batches[_batchAddress].annualInterestRate,
            _debtIncreaseFromRedist: 0,
            _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee,
            _debtChangeFromOperation: int256(_troveChange.debtIncrease),
            _collIncreaseFromRedist: 0,
            _collChangeFromOperation: int256(_troveChange.collIncrease)
        });

        emit BatchUpdated({
            _interestBatchManager: _batchAddress,
            _operation: BatchOperation.joinBatch,
            _debt: batches[_batchAddress].debt,
            _coll: batches[_batchAddress].coll,
            _annualInterestRate: batches[_batchAddress].annualInterestRate,
            _annualManagementFee: batches[_batchAddress].annualManagementFee,
            _totalDebtShares: batches[_batchAddress].totalDebtShares
        });
    }

    function setTroveStatusToActive(uint256 _troveId) external {
        _requireCallerIsBorrowerOperations();
        Troves[_troveId].status = Status.active;
    }

    function onAdjustTroveInterestRate(
        uint256 _troveId,
        uint256 _newColl,
        uint256 _newDebt,
        uint256 _newAnnualInterestRate,
        TroveChange calldata _troveChange
    ) external {
        _requireCallerIsBorrowerOperations();

        Troves[_troveId].coll = _newColl;
        Troves[_troveId].debt = _newDebt;
        Troves[_troveId].annualInterestRate = _newAnnualInterestRate;
        Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp);
        Troves[_troveId].lastInterestRateAdjTime = uint64(block.timestamp);

        _movePendingTroveRewardsToActivePool(
            defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain
        );

        _updateTroveRewardSnapshots(_troveId);

        emit TroveUpdated({
            _troveId: _troveId,
            _debt: _newDebt,
            _coll: _newColl,
            _stake: Troves[_troveId].stake,
            _annualInterestRate: _newAnnualInterestRate,
            _snapshotOfTotalCollRedist: L_coll,
            _snapshotOfTotalDebtRedist: L_boldDebt
        });

        emit TroveOperation({
            _troveId: _troveId,
            _operation: Operation.adjustTroveInterestRate,
            _annualInterestRate: _newAnnualInterestRate,
            _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain,
            _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee,
            _debtChangeFromOperation: 0,
            _collIncreaseFromRedist: _troveChange.appliedRedistCollGain,
            _collChangeFromOperation: 0
        });
    }

    function onAdjustTrove(uint256 _troveId, uint256 _newColl, uint256 _newDebt, TroveChange calldata _troveChange)
        external
    {
        _requireCallerIsBorrowerOperations();

        Troves[_troveId].coll = _newColl;
        Troves[_troveId].debt = _newDebt;
        Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp);

        _movePendingTroveRewardsToActivePool(
            defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain
        );

        uint256 newStake = _updateStakeAndTotalStakes(_troveId, _newColl);
        _updateTroveRewardSnapshots(_troveId);

        emit TroveUpdated({
            _troveId: _troveId,
            _debt: _newDebt,
            _coll: _newColl,
            _stake: newStake,
            _annualInterestRate: Troves[_troveId].annualInterestRate,
            _snapshotOfTotalCollRedist: L_coll,
            _snapshotOfTotalDebtRedist: L_boldDebt
        });

        emit TroveOperation({
            _troveId: _troveId,
            _operation: Operation.adjustTrove,
            _annualInterestRate: Troves[_troveId].annualInterestRate,
            _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain,
            _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee,
            _debtChangeFromOperation: int256(_troveChange.debtIncrease) - int256(_troveChange.debtDecrease),
            _collIncreaseFromRedist: _troveChange.appliedRedistCollGain,
            _collChangeFromOperation: int256(_troveChange.collIncrease) - int256(_troveChange.collDecrease)
        });
    }

    function onCloseTrove(
        uint256 _troveId,
        TroveChange memory _troveChange, // decrease vars: entire, with interest, batch fee and redistribution
        address _batchAddress,
        uint256 _newBatchColl,
        uint256 _newBatchDebt // entire, with interest and batch fee
    ) external override {
        _requireCallerIsBorrowerOperations();
        _closeTrove(_troveId, _troveChange, _batchAddress, _newBatchColl, _newBatchDebt, Status.closedByOwner);
        _movePendingTroveRewardsToActivePool(
            defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain
        );

        emit TroveUpdated({
            _troveId: _troveId,
            _debt: 0,
            _coll: 0,
            _stake: 0,
            _annualInterestRate: 0,
            _snapshotOfTotalCollRedist: 0,
            _snapshotOfTotalDebtRedist: 0
        });

        emit TroveOperation({
            _troveId: _troveId,
            _operation: Operation.closeTrove,
            _annualInterestRate: 0,
            _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain,
            _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee,
            _debtChangeFromOperation: int256(_troveChange.debtIncrease) - int256(_troveChange.debtDecrease),
            _collIncreaseFromRedist: _troveChange.appliedRedistCollGain,
            _collChangeFromOperation: int256(_troveChange.collIncrease) - int256(_troveChange.collDecrease)
        });

        if (_batchAddress != address(0)) {
            emit BatchUpdated({
                _interestBatchManager: _batchAddress,
                _operation: BatchOperation.exitBatch,
                _debt: batches[_batchAddress].debt,
                _coll: batches[_batchAddress].coll,
                _annualInterestRate: batches[_batchAddress].annualInterestRate,
                _annualManagementFee: batches[_batchAddress].annualManagementFee,
                _totalDebtShares: batches[_batchAddress].totalDebtShares
            });
        }
    }

    function _closeTrove(
        uint256 _troveId,
        TroveChange memory _troveChange, // decrease vars: entire, with interest, batch fee and redistribution
        address _batchAddress,
        uint256 _newBatchColl,
        uint256 _newBatchDebt, // entire, with interest and batch fee
        Status closedStatus
    ) internal {
        // assert(closedStatus == Status.closedByLiquidation || closedStatus == Status.closedByOwner);

        uint256 TroveIdsArrayLength = TroveIds.length;
        _requireMoreThanOneTroveInSystem(TroveIdsArrayLength);

        _removeTroveId(_troveId, TroveIdsArrayLength);

        Trove memory trove = Troves[_troveId];

        // If trove belongs to a batch, remove from it
        if (_batchAddress != address(0)) {
            if (trove.status == Status.active) {
                sortedTroves.removeFromBatch(_troveId);
            }

            _removeTroveSharesFromBatch(
                _troveId,
                _troveChange.collDecrease,
                _troveChange.debtDecrease,
                _troveChange,
                _batchAddress,
                _newBatchColl,
                _newBatchDebt
            );
        } else {
            if (trove.status == Status.active) {
                sortedTroves.remove(_troveId);
            }
        }

        uint256 newTotalStakes = totalStakes - trove.stake;
        totalStakes = newTotalStakes;

        // Zero Trove properties
        delete Troves[_troveId];
        Troves[_troveId].status = closedStatus;

        // Zero Trove snapshots
        delete rewardSnapshots[_troveId];

        // burn ERC721
        troveNFT.burn(_troveId);
    }

    function onAdjustTroveInsideBatch(
        uint256 _troveId,
        uint256 _newTroveColl, // entire, with redistribution and trove change
        TroveChange memory _troveChange,
        address _batchAddress,
        uint256 _newBatchColl, // without trove change
        uint256 _newBatchDebt // entire (with interest, batch fee), but without trove change nor upfront fee nor redistribution
    ) external {
        _requireCallerIsBorrowerOperations();

        // Trove
        Troves[_troveId].coll = _newTroveColl;
        _updateTroveRewardSnapshots(_troveId);
        uint256 newStake = _updateStakeAndTotalStakes(_troveId, _newTroveColl);

        // Batch
        _updateBatchShares(_troveId, _batchAddress, _troveChange, _newBatchColl, _newBatchDebt);

        _movePendingTroveRewardsToActivePool(
            defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain
        );

        emit BatchedTroveUpdated({
            _troveId: _troveId,
            _interestBatchManager: _batchAddress,
            _batchDebtShares: Troves[_troveId].batchDebtShares,
            _coll: _newTroveColl,
            _stake: newStake,
            _snapshotOfTotalCollRedist: L_coll,
            _snapshotOfTotalDebtRedist: L_boldDebt
        });

        emit TroveOperation({
            _troveId: _troveId,
            _operation: Operation.adjustTrove,
            _annualInterestRate: batches[_batchAddress].annualInterestRate,
            _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain,
            _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee,
            _debtChangeFromOperation: int256(_troveChange.debtIncrease) - int256(_troveChange.debtDecrease),
            _collIncreaseFromRedist: _troveChange.appliedRedistCollGain,
            _collChangeFromOperation: int256(_troveChange.collIncrease) - int256(_troveChange.collDecrease)
        });

        emit BatchUpdated({
            _interestBatchManager: _batchAddress,
            _operation: BatchOperation.troveChange,
            _debt: batches[_batchAddress].debt,
            _coll: batches[_batchAddress].coll,
            _annualInterestRate: batches[_batchAddress].annualInterestRate,
            _annualManagementFee: batches[_batchAddress].annualManagementFee,
            _totalDebtShares: batches[_batchAddress].totalDebtShares
        });
    }

    function onApplyTroveInterest(
        uint256 _troveId,
        uint256 _newTroveColl,
        uint256 _newTroveDebt,
        address _batchAddress,
        uint256 _newBatchColl,
        uint256 _newBatchDebt,
        TroveChange calldata _troveChange
    ) external {
        _requireCallerIsBorrowerOperations();

        Troves[_troveId].coll = _newTroveColl;

        if (_batchAddress != address(0)) {
            _updateBatchShares(_troveId, _batchAddress, _troveChange, _newBatchColl, _newBatchDebt);

            emit BatchUpdated({
                _interestBatchManager: _batchAddress,
                _operation: BatchOperation.applyBatchInterestAndFee,
                _debt: _newBatchDebt,
                _coll: _newBatchColl,
                _annualInterestRate: batches[_batchAddress].annualInterestRate,
                _annualManagementFee: batches[_batchAddress].annualManagementFee,
                _totalDebtShares: batches[_batchAddress].totalDebtShares
            });
        } else {
            Troves[_troveId].debt = _newTroveDebt;
            Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp);
        }

        _movePendingTroveRewardsToActivePool(
            defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain
        );

        _updateTroveRewardSnapshots(_troveId);

        emit TroveUpdated({
            _troveId: _troveId,
            _debt: _newTroveDebt,
            _coll: _newTroveColl,
            _stake: Troves[_troveId].stake,
            _annualInterestRate: Troves[_troveId].annualInterestRate,
            _snapshotOfTotalCollRedist: L_coll,
            _snapshotOfTotalDebtRedist: L_boldDebt
        });

        emit TroveOperation({
            _troveId: _troveId,
            _operation: Operation.applyPendingDebt,
            _annualInterestRate: Troves[_troveId].annualInterestRate,
            _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain,
            _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee,
            _debtChangeFromOperation: int256(_troveChange.debtIncrease) - int256(_troveChange.debtDecrease),
            _collIncreaseFromRedist: _troveChange.appliedRedistCollGain,
            _collChangeFromOperation: int256(_troveChange.collIncrease) - int256(_troveChange.collDecrease)
        });
    }

    function onRegisterBatchManager(address _account, uint256 _annualInterestRate, uint256 _annualManagementFee)
        external
    {
        _requireCallerIsBorrowerOperations();

        batches[_account].arrayIndex = uint64(batchIds.length);
        batches[_account].annualInterestRate = _annualInterestRate;
        batches[_account].annualManagementFee = _annualManagementFee;
        batches[_account].lastInterestRateAdjTime = uint64(block.timestamp);

        batchIds.push(_account);

        emit BatchUpdated({
            _interestBatchManager: _account,
            _operation: BatchOperation.registerBatchManager,
            _debt: 0,
            _coll: 0,
            _annualInterestRate: _annualInterestRate,
            _annualManagementFee: _annualManagementFee,
            _totalDebtShares: 0
        });
    }

    function onLowerBatchManagerAnnualFee(
        address _batchAddress,
        uint256 _newColl,
        uint256 _newDebt,
        uint256 _newAnnualManagementFee
    ) external {
        _requireCallerIsBorrowerOperations();

        batches[_batchAddress].coll = _newColl;
        batches[_batchAddress].debt = _newDebt;
        batches[_batchAddress].annualManagementFee = _newAnnualManagementFee;
        batches[_batchAddress].lastDebtUpdateTime = uint64(block.timestamp);

        emit BatchUpdated({
            _interestBatchManager: _batchAddress,
            _operation: BatchOperation.lowerBatchManagerAnnualFee,
            _debt: _newDebt,
            _coll: _newColl,
            _annualInterestRate: batches[_batchAddress].annualInterestRate,
            _annualManagementFee: _newAnnualManagementFee,
            _totalDebtShares: batches[_batchAddress].totalDebtShares
        });
    }

    function onSetBatchManagerAnnualInterestRate(
        address _batchAddress,
        uint256 _newColl,
        uint256 _newDebt,
        uint256 _newAnnualInterestRate
    ) external {
        _requireCallerIsBorrowerOperations();

        batches[_batchAddress].coll = _newColl;
        batches[_batchAddress].debt = _newDebt;
        batches[_batchAddress].annualInterestRate = _newAnnualInterestRate;
        batches[_batchAddress].lastDebtUpdateTime = uint64(block.timestamp);
        batches[_batchAddress].lastInterestRateAdjTime = uint64(block.timestamp);

        emit BatchUpdated({
            _interestBatchManager: _batchAddress,
            _operation: BatchOperation.setBatchManagerAnnualInterestRate,
            _debt: _newDebt,
            _coll: _newColl,
            _annualInterestRate: _newAnnualInterestRate,
            _annualManagementFee: batches[_batchAddress].annualManagementFee,
            _totalDebtShares: batches[_batchAddress].totalDebtShares
        });
    }

    function onSetInterestBatchManager(OnSetInterestBatchManagerParams calldata _params) external {
        _requireCallerIsBorrowerOperations();
        TroveChange memory _troveChange = _params.troveChange;

        // assert(batchIds[batches[_params.newBatchAddress].arrayIndex] == _params.newBatchAddress);

        _updateTroveRewardSnapshots(_params.troveId);

        // Clean Trove state
        Troves[_params.troveId].debt = 0;
        Troves[_params.troveId].annualInterestRate = 0;
        Troves[_params.troveId].lastDebtUpdateTime = 0;
        Troves[_params.troveId].coll = _params.troveColl;

        Troves[_params.troveId].interestBatchManager = _params.newBatchAddress;
        Troves[_params.troveId].lastInterestRateAdjTime = uint64(block.timestamp);

        _troveChange.collIncrease = _params.troveColl - _troveChange.appliedRedistCollGain;
        _troveChange.debtIncrease = _params.troveDebt - _troveChange.appliedRedistBoldDebtGain - _troveChange.upfrontFee;
        _updateBatchShares(
            _params.troveId, _params.newBatchAddress, _troveChange, _params.newBatchColl, _params.newBatchDebt
        );

        _movePendingTroveRewardsToActivePool(
            defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain
        );

        emit BatchedTroveUpdated({
            _troveId: _params.troveId,
            _interestBatchManager: _params.newBatchAddress,
            _batchDebtShares: Troves[_params.troveId].batchDebtShares,
            _coll: _params.troveColl,
            _stake: Troves[_params.troveId].stake,
            _snapshotOfTotalCollRedist: L_coll,
            _snapshotOfTotalDebtRedist: L_boldDebt
        });

        emit TroveOperation({
            _troveId: _params.troveId,
            _operation: Operation.setInterestBatchManager,
            _annualInterestRate: batches[_params.newBatchAddress].annualInterestRate,
            _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain,
            _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee,
            _debtChangeFromOperation: 0,
            _collIncreaseFromRedist: _troveChange.appliedRedistCollGain,
            _collChangeFromOperation: 0
        });

        emit BatchUpdated({
            _interestBatchManager: _params.newBatchAddress,
            _operation: BatchOperation.joinBatch,
            _debt: batches[_params.newBatchAddress].debt,
            _coll: batches[_params.newBatchAddress].coll,
            _annualInterestRate: batches[_params.newBatchAddress].annualInterestRate,
            _annualManagementFee: batches[_params.newBatchAddress].annualManagementFee,
            _totalDebtShares: batches[_params.newBatchAddress].totalDebtShares
        });
    }

    function _updateBatchShares(
        uint256 _troveId,
        address _batchAddress,
        TroveChange memory _troveChange,
        uint256 _batchColl, // without trove change
        uint256 _batchDebt // entire (with interest, batch fee), but without trove change, nor upfront fee nor redist
    ) internal {
        // Debt
        uint256 currentBatchDebtShares = batches[_batchAddress].totalDebtShares;
        uint256 batchDebtSharesDelta;
        uint256 debtIncrease =
            _troveChange.debtIncrease + _troveChange.upfrontFee + _troveChange.appliedRedistBoldDebtGain;
        uint256 debtDecrease;
        if (debtIncrease > _troveChange.debtDecrease) {
            debtIncrease -= _troveChange.debtDecrease;
        } else {
            debtDecrease = _troveChange.debtDecrease - debtIncrease;
            debtIncrease = 0;
        }

        if (debtIncrease == 0 && debtDecrease == 0) {
            batches[_batchAddress].debt = _batchDebt;
        } else {
            if (debtIncrease > 0) {
                // Add debt
                if (_batchDebt == 0) {
                    batchDebtSharesDelta = debtIncrease;
                } else {
                    batchDebtSharesDelta = currentBatchDebtShares * debtIncrease / _batchDebt;
                }

                Troves[_troveId].batchDebtShares += batchDebtSharesDelta;
                batches[_batchAddress].debt = _batchDebt + debtIncrease;
                batches[_batchAddress].totalDebtShares = currentBatchDebtShares + batchDebtSharesDelta;
            } else if (debtDecrease > 0) {
                // Subtract debt
                batchDebtSharesDelta = currentBatchDebtShares * debtDecrease / _batchDebt;

                Troves[_troveId].batchDebtShares -= batchDebtSharesDelta;
                batches[_batchAddress].debt = _batchDebt - debtDecrease;
                batches[_batchAddress].totalDebtShares = currentBatchDebtShares - batchDebtSharesDelta;
            }
        }
        // Update debt checkpoint
        batches[_batchAddress].lastDebtUpdateTime = uint64(block.timestamp);

        // Collateral
        uint256 collIncrease = _troveChange.collIncrease + _troveChange.appliedRedistCollGain;
        uint256 collDecrease;
        if (collIncrease > _troveChange.collDecrease) {
            collIncrease -= _troveChange.collDecrease;
        } else {
            collDecrease = _troveChange.collDecrease - collIncrease;
            collIncrease = 0;
        }

        if (collIncrease == 0 && collDecrease == 0) {
            batches[_batchAddress].coll = _batchColl;
        } else {
            if (collIncrease > 0) {
                // Add coll
                batches[_batchAddress].coll = _batchColl + collIncrease;
            } else if (collDecrease > 0) {
                // Subtract coll
                batches[_batchAddress].coll = _batchColl - collDecrease;
            }
        }
    }

    function onRemoveFromBatch(
        uint256 _troveId,
        uint256 _newTroveColl, // entire, with redistribution
        uint256 _newTroveDebt, // entire, with interest, batch fee and redistribution
        TroveChange memory _troveChange,
        address _batchAddress,
        uint256 _newBatchColl,
        uint256 _newBatchDebt, // entire, with interest and batch fee
        uint256 _newAnnualInterestRate
    ) external {
        _requireCallerIsBorrowerOperations();
        // assert(batchIds[batches[_batchAddress].arrayIndex] == _batchAddress);

        // Subtract from batch
        _removeTroveSharesFromBatch(
            _troveId, _newTroveColl, _newTroveDebt, _troveChange, _batchAddress, _newBatchColl, _newBatchDebt
        );

        // Restore Trove state
        Troves[_troveId].debt = _newTroveDebt;
        Troves[_troveId].coll = _newTroveColl;
        Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp);
        Troves[_troveId].annualInterestRate = _newAnnualInterestRate;
        Troves[_troveId].lastInterestRateAdjTime = uint64(block.timestamp);

        _updateTroveRewardSnapshots(_troveId);
        _movePendingTroveRewardsToActivePool(
            defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain
        );

        emit TroveUpdated({
            _troveId: _troveId,
            _debt: _newTroveDebt,
            _coll: _newTroveColl,
            _stake: Troves[_troveId].stake,
            _annualInterestRate: _newAnnualInterestRate,
            _snapshotOfTotalCollRedist: L_coll,
            _snapshotOfTotalDebtRedist: L_boldDebt
        });

        emit TroveOperation({
            _troveId: _troveId,
            _operation: Operation.removeFromBatch,
            _annualInterestRate: _newAnnualInterestRate,
            _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain,
            _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee,
            _debtChangeFromOperation: 0,
            _collIncreaseFromRedist: _troveChange.appliedRedistCollGain,
            _collChangeFromOperation: 0
        });

        emit BatchUpdated({
            _interestBatchManager: _batchAddress,
            _operation: BatchOperation.exitBatch,
            _debt: batches[_batchAddress].debt,
            _coll: batches[_batchAddress].coll,
            _annualInterestRate: batches[_batchAddress].annualInterestRate,
            _annualManagementFee: batches[_batchAddress].annualManagementFee,
            _totalDebtShares: batches[_batchAddress].totalDebtShares
        });
    }

    function _removeTroveSharesFromBatch(
        uint256 _troveId,
        uint256 _newTroveColl, // entire, with redistribution
        uint256 _newTroveDebt, // entire, with interest, batch fee and redistribution
        TroveChange memory _troveChange,
        address _batchAddress,
        uint256 _newBatchColl, // without trove change
        uint256 _newBatchDebt // entire (with interest and batch fee), but without trove change
    ) internal {
        // As we are removing:
        // assert(_newBatchDebt > 0 || _newBatchColl > 0);

        Trove memory trove = Troves[_troveId];

        // We don’t need to increase the shares corresponding to redistribution first, because they would be subtracted immediately after
        // We don’t need to account for interest nor batch fee because it’s proportional to debt shares
        uint256 batchDebtDecrease = _newTroveDebt - _troveChange.upfrontFee - _troveChange.appliedRedistBoldDebtGain;
        uint256 batchCollDecrease = _newTroveColl - _troveChange.appliedRedistCollGain;

        batches[_batchAddress].totalDebtShares -= trove.batchDebtShares;
        batches[_batchAddress].debt = _newBatchDebt - batchDebtDecrease;
        batches[_batchAddress].coll = _newBatchColl - batchCollDecrease;
        batches[_batchAddress].lastDebtUpdateTime = uint64(block.timestamp);

        Troves[_troveId].interestBatchManager = address(0);
        Troves[_troveId].batchDebtShares = 0;
    }
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.18;

import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

import "./Interfaces/IBorrowerOperations.sol";
import "./Interfaces/IAddressesRegistry.sol";
import "./Interfaces/ITroveManager.sol";
import "./Interfaces/IBoldToken.sol";
import "./Interfaces/ICollSurplusPool.sol";
import "./Interfaces/ISortedTroves.sol";
import "./Dependencies/LiquityBase.sol";
import "./Dependencies/AddRemoveManagers.sol";
import "./Types/LatestTroveData.sol";
import "./Types/LatestBatchData.sol";

// import "forge-std/console2.sol";

contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperations {
    using SafeERC20 for IERC20;

    // --- Connected contract declarations ---

    IERC20 internal immutable collToken;
    ITroveManager internal troveManager;
    address internal gasPoolAddress;
    ICollSurplusPool internal collSurplusPool;
    IBoldToken internal boldToken;
    // A doubly linked list of Troves, sorted by their collateral ratios
    ISortedTroves internal sortedTroves;
    // Wrapped ETH for liquidation reserve (gas compensation)
    IWETH internal immutable WETH;

    // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied
    uint256 public immutable CCR;

    // Shutdown system collateral ratio. If the system's total collateral ratio (TCR) for a given collateral falls below the SCR,
    // the protocol triggers the shutdown of the borrow market and permanently disables all borrowing operations except for closing Troves.
    uint256 public immutable SCR;
    bool public hasBeenShutDown;

    // Minimum collateral ratio for individual troves
    uint256 public immutable MCR;

    /*
    * Mapping from TroveId to individual delegate for interest rate setting.
    *
    * This address then has the ability to update the borrower’s interest rate, but not change its debt or collateral.
    * Useful for instance for cold/hot wallet setups.
    */
    mapping(uint256 => InterestIndividualDelegate) private interestIndividualDelegateOf;

    /*
     * Mapping from TroveId to granted address for interest rate setting (batch manager).
     *
     * Batch managers set the interest rate for every Trove in the batch. The interest rate is the same for all Troves in the batch.
     */
    mapping(uint256 => address) public interestBatchManagerOf;

    // List of registered Interest Batch Managers
    mapping(address => InterestBatchManager) private interestBatchManagers;

    /* --- Variable container structs  ---

    Used to hold, return and assign variables inside a function, in order to avoid the error:
    "CompilerError: Stack too deep". */

    struct OpenTroveVars {
        ITroveManager troveManager;
        uint256 troveId;
        TroveChange change;
        LatestBatchData batch;
    }

    struct LocalVariables_openTrove {
        ITroveManager troveManager;
        IActivePool activePool;
        IBoldToken boldToken;
        uint256 troveId;
        uint256 price;
        uint256 avgInterestRate;
        uint256 entireDebt;
        uint256 ICR;
        uint256 newTCR;
        bool newOracleFailureDetected;
    }

    struct LocalVariables_adjustTrove {
        IActivePool activePool;
        IBoldToken boldToken;
        LatestTroveData trove;
        uint256 price;
        bool isBelowCriticalThreshold;
        uint256 newICR;
        uint256 newDebt;
        uint256 newColl;
        bool newOracleFailureDetected;
    }

    struct LocalVariables_setInterestBatchManager {
        ITroveManager troveManager;
        IActivePool activePool;
        ISortedTroves sortedTroves;
        address oldBatchManager;
        LatestTroveData trove;
        LatestBatchData oldBatch;
        LatestBatchData newBatch;
    }

    struct LocalVariables_removeFromBatch {
        ITroveManager troveManager;
        ISortedTroves sortedTroves;
        address batchManager;
        LatestTroveData trove;
        LatestBatchData batch;
        uint256 newBatchDebt;
    }

    error IsShutDown();
    error NotShutDown();
    error TCRNotBelowSCR();
    error ZeroAdjustment();
    error NotOwnerNorInterestManager();
    error TroveInBatch();
    error TroveNotInBatch();
    error InterestNotInRange();
    error BatchInterestRateChangePeriodNotPassed();
    error TroveNotOpen();
    error TroveNotActive();
    error TroveNotUnredeemable();
    error TroveOpen();
    error UpfrontFeeTooHigh();
    error BelowCriticalThreshold();
    error BorrowingNotPermittedBelowCT();
    error ICRBelowMCR();
    error RepaymentNotMatchingCollWithdrawal();
    error TCRBelowCCR();
    error DebtBelowMin();
    error CollWithdrawalTooHigh();
    error NotEnoughBoldBalance();
    error InterestRateTooLow();
    error InterestRateTooHigh();
    error InterestRateNotNew();
    error InvalidInterestBatchManager();
    error BatchManagerExists();
    error BatchManagerNotNew();
    error NewFeeNotLower();
    error CallerNotTroveManager();
    error CallerNotPriceFeed();
    error MinGeMax();
    error AnnualManagementFeeTooHigh();
    error MinInterestRateChangePeriodTooLow();
    error NewOracleFailureDetected();

    event TroveManagerAddressChanged(address _newTroveManagerAddress);
    event GasPoolAddressChanged(address _gasPoolAddress);
    event CollSurplusPoolAddressChanged(address _collSurplusPoolAddress);
    event SortedTrovesAddressChanged(address _sortedTrovesAddress);
    event BoldTokenAddressChanged(address _boldTokenAddress);

    event ShutDown(uint256 _tcr);
    event ShutDownFromOracleFailure(address _oracleAddress);

    constructor(IAddressesRegistry _addressesRegistry) AddRemoveManagers(_addressesRegistry) LiquityBase(_addressesRegistry) {
        // This makes impossible to open a trove with zero withdrawn Bold
        assert(MIN_DEBT > 0);

        collToken = _addressesRegistry.collToken();

        WETH = _addressesRegistry.WETH();

        CCR = _addressesRegistry.CCR();
        SCR = _addressesRegistry.SCR();
        MCR = _addressesRegistry.MCR();

        troveManager = _addressesRegistry.troveManager();
        gasPoolAddress = _addressesRegistry.gasPoolAddress();
        collSurplusPool = _addressesRegistry.collSurplusPool();
        sortedTroves = _addressesRegistry.sortedTroves();
        boldToken = _addressesRegistry.boldToken();

        emit TroveManagerAddressChanged(address(troveManager));
        emit GasPoolAddressChanged(gasPoolAddress);
        emit CollSurplusPoolAddressChanged(address(collSurplusPool));
        emit SortedTrovesAddressChanged(address(sortedTroves));
        emit BoldTokenAddressChanged(address(boldToken));

        // Allow funds movements between Liquity contracts
        collToken.approve(address(activePool), type(uint256).max);
    }

    // --- Borrower Trove Operations ---

    function openTrove(
        address _owner,
        uint256 _ownerIndex,
        uint256 _collAmount,
        uint256 _boldAmount,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _annualInterestRate,
        uint256 _maxUpfrontFee,
        address _addManager,
        address _removeManager,
        address _receiver
    ) external override returns (uint256) {
        _requireValidAnnualInterestRate(_annualInterestRate);

        OpenTroveVars memory vars;

        vars.troveId = _openTrove(
            _owner,
            _ownerIndex,
            _collAmount,
            _boldAmount,
            _annualInterestRate,
            address(0),
            0,
            0,
            _maxUpfrontFee,
            _addManager,
            _removeManager,
            _receiver,
            vars.change
        );

        // Set the stored Trove properties and mint the NFT
        troveManager.onOpenTrove(_owner, vars.troveId, vars.change, _annualInterestRate);

        sortedTroves.insert(vars.troveId, _annualInterestRate, _upperHint, _lowerHint);

        return vars.troveId;
    }

    function openTroveAndJoinInterestBatchManager(OpenTroveAndJoinInterestBatchManagerParams calldata _params)
        external
        override
        returns (uint256)
    {
        _requireValidInterestBatchManager(_params.interestBatchManager);

        OpenTroveVars memory vars;
        vars.troveManager = troveManager;

        vars.batch = vars.troveManager.getLatestBatchData(_params.interestBatchManager);

        // We set old weighted values here, as it’s only necessary for batches, so we don’t need to pass them to _openTrove func
        vars.change.batchAccruedManagementFee = vars.batch.accruedManagementFee;
        vars.change.oldWeightedRecordedDebt = vars.batch.weightedRecordedDebt;
        vars.change.oldWeightedRecordedBatchManagementFee = vars.batch.weightedRecordedBatchManagementFee;
        vars.troveId = _openTrove(
            _params.owner,
            _params.ownerIndex,
            _params.collAmount,
            _params.boldAmount,
            vars.batch.annualInterestRate,
            _params.interestBatchManager,
            vars.batch.entireDebtWithoutRedistribution,
            vars.batch.annualManagementFee,
            _params.maxUpfrontFee,
            _params.addManager,
            _params.removeManager,
            _params.receiver,
            vars.change
        );

        interestBatchManagerOf[vars.troveId] = _params.interestBatchManager;

        // Set the stored Trove properties and mint the NFT
        vars.troveManager.onOpenTroveAndJoinBatch(
            _params.owner,
            vars.troveId,
            vars.change,
            _params.interestBatchManager,
            vars.batch.entireCollWithoutRedistribution,
            vars.batch.entireDebtWithoutRedistribution
        );

        sortedTroves.insertIntoBatch(
            vars.troveId,
            BatchId.wrap(_params.interestBatchManager),
            vars.batch.annualInterestRate,
            _params.upperHint,
            _params.lowerHint
        );

        return vars.troveId;
    }

    function _openTrove(
        address _owner,
        uint256 _ownerIndex,
        uint256 _collAmount,
        uint256 _boldAmount,
        uint256 _annualInterestRate,
        address _interestBatchManager,
        uint256 _batchEntireDebt,
        uint256 _batchManagementAnnualFee,
        uint256 _maxUpfrontFee,
        address _addManager,
        address _removeManager,
        address _receiver,
        TroveChange memory _change
    ) internal returns (uint256) {
        _requireIsNotShutDown();

        LocalVariables_openTrove memory vars;

        // TODO: stack too deep not allowing to reuse troveManager from outer functions
        vars.troveManager = troveManager;
        vars.activePool = activePool;
        vars.boldToken = boldToken;

        vars.price = _requireOraclesLive();

        // --- Checks ---

        _requireNotBelowCriticalThreshold(vars.price);

        vars.troveId = uint256(keccak256(abi.encode(_owner, _ownerIndex)));
        _requireTroveIsNotOpen(vars.troveManager, vars.troveId);

        _change.collIncrease = _collAmount;
        _change.debtIncrease = _boldAmount;

        // For simplicity, we ignore the fee when calculating the approx. interest rate
        _change.newWeightedRecordedDebt = (_batchEntireDebt + _change.debtIncrease) * _annualInterestRate;

        vars.avgInterestRate = vars.activePool.getNewApproxAvgInterestRateFromTroveChange(_change);
        _change.upfrontFee = _calcUpfrontFee(_change.debtIncrease, vars.avgInterestRate);
        _requireUserAcceptsUpfrontFee(_change.upfrontFee, _maxUpfrontFee);

        vars.entireDebt = _change.debtIncrease + _change.upfrontFee;
        _requireAtLeastMinDebt(vars.entireDebt);

        // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee, and the batch fee if needed
        if (_interestBatchManager == address(0)) {
            _change.newWeightedRecordedDebt = vars.entireDebt * _annualInterestRate;
        } else {
            // old values have been set outside, before calling this function
            _change.newWeightedRecordedDebt = (_batchEntireDebt + vars.entireDebt) * _annualInterestRate;
            _change.newWeightedRecordedBatchManagementFee =
                (_batchEntireDebt + vars.entireDebt) * _batchManagementAnnualFee;
        }

        // ICR is based on the composite debt, i.e. the requested Bold amount + Bold gas comp + upfront fee.
        vars.ICR = LiquityMath._computeCR(_collAmount, vars.entireDebt, vars.price);
        _requireICRisAboveMCR(vars.ICR);

        vars.newTCR = _getNewTCRFromTroveChange(_change, vars.price);
        _requireNewTCRisAboveCCR(vars.newTCR);

        // --- Effects & interactions ---

        // Set add/remove managers
        // TODO: We can restore the condition for non-zero managers if we end up ipmlementing at least one of:
        // - wipe them out on closing troves
        // - do not reuse troveIds
        // for now it is safer to make sure they are set
        _setAddManager(vars.troveId, _addManager);
        _setRemoveManagerAndReceiver(vars.troveId, _removeManager, _receiver);

        vars.activePool.mintAggInterestAndAccountForTroveChange(_change, _interestBatchManager);

        // Pull coll tokens from sender and move them to the Active Pool
        _pullCollAndSendToActivePool(vars.activePool, _collAmount);

        // Mint the requested _boldAmount to the borrower and mint the gas comp to the GasPool
        vars.boldToken.mint(msg.sender, _boldAmount);
        WETH.transferFrom(msg.sender, gasPoolAddress, ETH_GAS_COMPENSATION);

        return vars.troveId;
    }

    // Send collateral to a trove
    function addColl(uint256 _troveId, uint256 _collAmount) external override {
        ITroveManager troveManagerCached = troveManager;
        _requireTroveIsActive(troveManagerCached, _troveId);

        TroveChange memory troveChange;
        troveChange.collIncrease = _collAmount;

        _adjustTrove(
            troveManagerCached,
            _troveId,
            troveChange,
            0 // _maxUpfrontFee
        );
    }

    // Withdraw collateral from a trove
    function withdrawColl(uint256 _troveId, uint256 _collWithdrawal) external override {
        ITroveManager troveManagerCached = troveManager;
        _requireTroveIsActive(troveManagerCached, _troveId);

        TroveChange memory troveChange;
        troveChange.collDecrease = _collWithdrawal;

        _adjustTrove(
            troveManagerCached,
            _troveId,
            troveChange,
            0 // _maxUpfrontFee
        );
    }

    // Withdraw Bold tokens from a trove: mint new Bold tokens to the owner, and increase the trove's debt accordingly
    function withdrawBold(uint256 _troveId, uint256 _boldAmount, uint256 _maxUpfrontFee) external override {
        ITroveManager troveManagerCached = troveManager;
        _requireTroveIsActive(troveManagerCached, _troveId);

        TroveChange memory troveChange;
        troveChange.debtIncrease = _boldAmount;
        _adjustTrove(troveManagerCached, _troveId, troveChange, _maxUpfrontFee);
    }

    // Repay Bold tokens to a Trove: Burn the repaid Bold tokens, and reduce the trove's debt accordingly
    function repayBold(uint256 _troveId, uint256 _boldAmount) external override {
        ITroveManager troveManagerCached = troveManager;
        _requireTroveIsActive(troveManagerCached, _troveId);

        TroveChange memory troveChange;
        troveChange.debtDecrease = _boldAmount;

        _adjustTrove(
            troveManagerCached,
            _troveId,
            troveChange,
            0 // _maxUpfrontFee
        );
    }

    function _initTroveChange(
        TroveChange memory _troveChange,
        uint256 _collChange,
        bool _isCollIncrease,
        uint256 _boldChange,
        bool _isDebtIncrease
    ) internal pure {
        if (_isCollIncrease) {
            _troveChange.collIncrease = _collChange;
        } else {
            _troveChange.collDecrease = _collChange;
        }

        if (_isDebtIncrease) {
            _troveChange.debtIncrease = _boldChange;
        } else {
            _troveChange.debtDecrease = _boldChange;
        }
    }

    function adjustTrove(
        uint256 _troveId,
        uint256 _collChange,
        bool _isCollIncrease,
        uint256 _boldChange,
        bool _isDebtIncrease,
        uint256 _maxUpfrontFee
    ) external override {
        ITroveManager troveManagerCached = troveManager;
        _requireTroveIsActive(troveManagerCached, _troveId);

        TroveChange memory troveChange;
        _initTroveChange(troveChange, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease);
        _adjustTrove(troveManagerCached, _troveId, troveChange, _maxUpfrontFee);
    }

    function adjustUnredeemableTrove(
        uint256 _troveId,
        uint256 _collChange,
        bool _isCollIncrease,
        uint256 _boldChange,
        bool _isDebtIncrease,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _maxUpfrontFee
    ) external override {
        ITroveManager troveManagerCached = troveManager;
        _requireTroveIsUnredeemable(troveManagerCached, _troveId);

        TroveChange memory troveChange;
        _initTroveChange(troveChange, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease);
        _adjustTrove(troveManagerCached, _troveId, troveChange, _maxUpfrontFee);

        troveManagerCached.setTroveStatusToActive(_troveId);

        address batchManager = interestBatchManagerOf[_troveId];
        uint256 batchAnnualInteresRate;
        if (batchManager != address(0)) {
            LatestBatchData memory batch = troveManagerCached.getLatestBatchData(batchManager);
            batchAnnualInteresRate = batch.annualInterestRate;
        }
        _reInsertIntoSortedTroves(
            _troveId,
            troveManagerCached.getTroveAnnualInterestRate(_troveId),
            _upperHint,
            _lowerHint,
            batchManager,
            batchAnnualInteresRate
        );
    }

    function adjustTroveInterestRate(
        uint256 _troveId,
        uint256 _newAnnualInterestRate,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _maxUpfrontFee
    ) external {
        _requireIsNotShutDown();

        ITroveManager troveManagerCached = troveManager;

        _requireValidAnnualInterestRate(_newAnnualInterestRate);
        _requireIsNotInBatch(_troveId);
        address owner = troveNFT.ownerOf(_troveId);
        _requireSenderIsOwnerOrInterestManager(_troveId, owner);
        _requireInterestRateInDelegateRange(_troveId, _newAnnualInterestRate);
        _requireTroveIsActive(troveManagerCached, _troveId);

        LatestTroveData memory trove = troveManagerCached.getLatestTroveData(_troveId);
        _requireAnnualInterestRateIsNew(trove.annualInterestRate, _newAnnualInterestRate);

        uint256 newDebt = trove.entireDebt;

        TroveChange memory troveChange;
        troveChange.appliedRedistBoldDebtGain = trove.redistBoldDebtGain;
        troveChange.appliedRedistCollGain = trove.redistCollGain;
        troveChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate;
        troveChange.oldWeightedRecordedDebt = trove.weightedRecordedDebt;

        // Apply upfront fee on premature adjustments
        if (
            trove.annualInterestRate != _newAnnualInterestRate
                && block.timestamp < trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN
        ) {
            newDebt = _applyUpfrontFee(trove.entireColl, newDebt, troveChange, _maxUpfrontFee);
        }

        // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee
        troveChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate;

        activePool.mintAggInterestAndAccountForTroveChange(troveChange, address(0));

        sortedTroves.reInsert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint);
        troveManagerCached.onAdjustTroveInterestRate(
            _troveId, trove.entireColl, newDebt, _newAnnualInterestRate, troveChange
        );
    }

    /*
    * _adjustTrove(): Alongside a debt change, this function can perform either a collateral top-up or a collateral withdrawal.
    */
    function _adjustTrove(
        ITroveManager _troveManager,
        uint256 _troveId,
        TroveChange memory _troveChange,
        uint256 _maxUpfrontFee
    ) internal {
        _requireIsNotShutDown();

        LocalVariables_adjustTrove memory vars;
        vars.activePool = activePool;
        vars.boldToken = boldToken;

        vars.price = _requireOraclesLive();
        vars.isBelowCriticalThreshold = _checkBelowCriticalThreshold(vars.price, CCR);

        // --- Checks ---

        _requireTroveIsOpen(_troveManager, _troveId);

        address owner = troveNFT.ownerOf(_troveId);
        address receiver = owner; // If it’s a withdrawal, and manager has receive privilege, manager would be the receiver

        if (_troveChange.collDecrease > 0 || _troveChange.debtIncrease > 0) {
            receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner);
        }

        if (_troveChange.collIncrease > 0 || _troveChange.debtDecrease > 0) {
            _requireSenderIsOwnerOrAddManager(_troveId, owner);
        }

        vars.trove = _troveManager.getLatestTroveData(_troveId);

        // When the adjustment is a debt repayment, check it's a valid amount and that the caller has enough Bold
        if (_troveChange.debtDecrease > 0) {
            uint256 maxRepayment = vars.trove.entireDebt > MIN_DEBT ? vars.trove.entireDebt - MIN_DEBT : 0;
            if (_troveChange.debtDecrease > maxRepayment) {
                _troveChange.debtDecrease = maxRepayment;
            }
            _requireSufficientBoldBalance(vars.boldToken, msg.sender, _troveChange.debtDecrease);
        }

        _requireNonZeroAdjustment(_troveChange);

        // When the adjustment is a collateral withdrawal, check that it's no more than the Trove's entire collateral
        if (_troveChange.collDecrease > 0) {
            _requireValidCollWithdrawal(vars.trove.entireColl, _troveChange.collDecrease);
        }

        vars.newColl = vars.trove.entireColl + _troveChange.collIncrease - _troveChange.collDecrease;
        vars.newDebt = vars.trove.entireDebt + _troveChange.debtIncrease - _troveChange.debtDecrease;

        address batchManager = interestBatchManagerOf[_troveId];
        bool isTroveInBatch = batchManager != address(0);
        LatestBatchData memory batch;
        uint256 batchFutureDebt;
        if (isTroveInBatch) {
            batch = _troveManager.getLatestBatchData(batchManager);

            batchFutureDebt = batch.entireDebtWithoutRedistribution + vars.trove.redistBoldDebtGain
                + _troveChange.debtIncrease - _troveChange.debtDecrease;

            // TODO: comment
            _troveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain;
            _troveChange.appliedRedistCollGain = vars.trove.redistCollGain;
            _troveChange.batchAccruedManagementFee = batch.accruedManagementFee;
            _troveChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt;
            _troveChange.newWeightedRecordedDebt = batchFutureDebt * batch.annualInterestRate;
            _troveChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee;
            _troveChange.newWeightedRecordedBatchManagementFee = batchFutureDebt * batch.annualManagementFee;
        } else {
            _troveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain;
            _troveChange.appliedRedistCollGain = vars.trove.redistCollGain;
            _troveChange.oldWeightedRecordedDebt = vars.trove.weightedRecordedDebt;
            _troveChange.newWeightedRecordedDebt = vars.newDebt * vars.trove.annualInterestRate;
        }

        // Pay an upfront fee on debt increases
        if (_troveChange.debtIncrease > 0) {
            uint256 avgInterestRate = vars.activePool.getNewApproxAvgInterestRateFromTroveChange(_troveChange);
            _troveChange.upfrontFee = _calcUpfrontFee(_troveChange.debtIncrease, avgInterestRate);
            _requireUserAcceptsUpfrontFee(_troveChange.upfrontFee, _maxUpfrontFee);

            vars.newDebt += _troveChange.upfrontFee;
            if (isTroveInBatch) {
                batchFutureDebt += _troveChange.upfrontFee;
                // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee
                _troveChange.newWeightedRecordedDebt = batchFutureDebt * batch.annualInterestRate;
                _troveChange.newWeightedRecordedBatchManagementFee = batchFutureDebt * batch.annualManagementFee;
            } else {
                // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee
                _troveChange.newWeightedRecordedDebt = vars.newDebt * vars.trove.annualInterestRate;
            }
        }

        // Make sure the Trove doesn't end up unredeemable
        // Now the max repayment is capped to stay above MIN_DEBT, so this only applies to adjustUnredeemableTrove
        _requireAtLeastMinDebt(vars.newDebt);

        vars.newICR = LiquityMath._computeCR(vars.newColl, vars.newDebt, vars.price);

        // Check the adjustment satisfies all conditions for the current system mode
        _requireValidAdjustmentInCurrentMode(_troveChange, vars);

        // --- Effects and interactions ---

        if (isTroveInBatch) {
            _troveManager.onAdjustTroveInsideBatch(
                _troveId,
                vars.newColl,
                _troveChange,
                batchManager,
                batch.entireCollWithoutRedistribution,
                batch.entireDebtWithoutRedistribution
            );
        } else {
            _troveManager.onAdjustTrove(_troveId, vars.newColl, vars.newDebt, _troveChange);
        }

        vars.activePool.mintAggInterestAndAccountForTroveChange(_troveChange, batchManager);
        _moveTokensFromAdjustment(receiver, _troveChange, vars.boldToken, vars.activePool);
    }

    function closeTrove(uint256 _troveId) external override {
        ITroveManager troveManagerCached = troveManager;
        IActivePool activePoolCached = activePool;
        IBoldToken boldTokenCached = boldToken;

        // --- Checks ---

        address owner = troveNFT.ownerOf(_troveId);
        address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner);
        _requireTroveIsOpen(troveManagerCached, _troveId);

        LatestTroveData memory trove = troveManagerCached.getLatestTroveData(_troveId);

        // The borrower must repay their entire debt including accrued interest, batch fee and redist. gains
        _requireSufficientBoldBalance(boldTokenCached, msg.sender, trove.entireDebt);

        TroveChange memory troveChange;
        troveChange.appliedRedistBoldDebtGain = trove.redistBoldDebtGain;
        troveChange.appliedRedistCollGain = trove.redistCollGain;
        troveChange.collDecrease = trove.entireColl;
        troveChange.debtDecrease = trove.entireDebt;

        address batchManager = interestBatchManagerOf[_troveId];
        LatestBatchData memory batch;
        if (batchManager != address(0)) {
            batch = troveManagerCached.getLatestBatchData(batchManager);
            uint256 batchFutureDebt =
                batch.entireDebtWithoutRedistribution - (trove.entireDebt - trove.redistBoldDebtGain);
            troveChange.batchAccruedManagementFee = batch.accruedManagementFee;
            troveChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt;
            troveChange.newWeightedRecordedDebt = batchFutureDebt * batch.annualInterestRate;
            troveChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee;
            troveChange.newWeightedRecordedBatchManagementFee = batchFutureDebt * batch.annualManagementFee;
        } else {
            troveChange.oldWeightedRecordedDebt = trove.weightedRecordedDebt;
            // troveChange.newWeightedRecordedDebt = 0;
        }

        (uint256 price, ) = priceFeed.fetchPrice();
        uint256 newTCR = _getNewTCRFromTroveChange(troveChange, price);
        if (!hasBeenShutDown) _requireNewTCRisAboveCCR(newTCR);

        troveManagerCached.onCloseTrove(
            _troveId,
            troveChange,
            batchManager,
            batch.entireCollWithoutRedistribution,
            batch.entireDebtWithoutRedistribution
        );

        // If trove is in batch
        if (batchManager != address(0)) {
            // Unlink here in BorrowerOperations
            interestBatchManagerOf[_troveId] = address(0);
        }

        activePoolCached.mintAggInterestAndAccountForTroveChange(troveChange, batchManager);

        // Return ETH gas compensation
        WETH.transferFrom(gasPoolAddress, receiver, ETH_GAS_COMPENSATION);
        // Burn the remainder of the Trove's entire debt from the user
        boldTokenCached.burn(msg.sender, trove.entireDebt);

        // Send the collateral back to the user
        activePoolCached.sendColl(receiver, trove.entireColl);

        _wipeTroveMappings(_troveId);
    }

    function applyPendingDebt(uint256 _troveId, uint256 _lowerHint, uint256 _upperHint) public {
        _requireIsNotShutDown();

        ITroveManager troveManagerCached = troveManager;

        _requireTroveIsOpen(troveManagerCached, _troveId);

        LatestTroveData memory trove = troveManagerCached.getLatestTroveData(_troveId);
        TroveChange memory change;
        change.appliedRedistBoldDebtGain = trove.redistBoldDebtGain;
        change.appliedRedistCollGain = trove.redistCollGain;

        address batchManager = interestBatchManagerOf[_troveId];
        LatestBatchData memory batch;

        if (batchManager == address(0)) {
            change.oldWeightedRecordedDebt = trove.weightedRecordedDebt;
            change.newWeightedRecordedDebt = trove.entireDebt * trove.annualInterestRate;
        } else {
            batch = troveManagerCached.getLatestBatchData(batchManager);
            change.batchAccruedManagementFee = batch.accruedManagementFee;
            change.oldWeightedRecordedDebt = batch.weightedRecordedDebt;
            change.newWeightedRecordedDebt =
                (batch.entireDebtWithoutRedistribution + trove.redistBoldDebtGain) * batch.annualInterestRate;
            change.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee;
            change.newWeightedRecordedBatchManagementFee =
                (batch.entireDebtWithoutRedistribution + trove.redistBoldDebtGain) * batch.annualManagementFee;
        }

        troveManagerCached.onApplyTroveInterest(
            _troveId,
            trove.entireColl,
            trove.entireDebt,
            batchManager,
            batch.entireCollWithoutRedistribution,
            batch.entireDebtWithoutRedistribution,
            change
        );
        activePool.mintAggInterestAndAccountForTroveChange(change, batchManager);

        // If the trove was unredeemable, and now it’s not anymore, put it back in the list
        if (_checkTroveIsUnredeemable(troveManagerCached, _troveId) && trove.entireDebt >= MIN_DEBT) {
            troveManagerCached.setTroveStatusToActive(_troveId);
            _reInsertIntoSortedTroves(
                _troveId, trove.annualInterestRate, _upperHint, _lowerHint, batchManager, batch.annualInterestRate
            );
        }
    }

    function getInterestIndividualDelegateOf(uint256 _troveId)
        external
        view
        returns (InterestIndividualDelegate memory)
    {
        return interestIndividualDelegateOf[_troveId];
    }

    function setInterestIndividualDelegate(
        uint256 _troveId,
        address _delegate,
        uint128 _minInterestRate,
        uint128 _maxInterestRate,
        // only needed if trove was previously in a batch:
        uint256 _newAnnualInterestRate,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _maxUpfrontFee
    ) external {
        _requireIsNotShutDown();
        _requireCallerIsBorrower(_troveId);
        interestIndividualDelegateOf[_troveId] =
            InterestIndividualDelegate(_delegate, _minInterestRate, _maxInterestRate);
        // Can’t have both individual delegation and batch manager
        if (interestBatchManagerOf[_troveId] != address(0)) {
            removeFromBatch(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee);
        }
    }

    function removeInterestIndividualDelegate(uint256 _troveId) external {
        _requireCallerIsBorrower(_troveId);
        delete interestIndividualDelegateOf[_troveId];
    }

    function getInterestBatchManager(address _account) external view returns (InterestBatchManager memory) {
        return interestBatchManagers[_account];
    }

    function registerBatchManager(
        uint128 _minInterestRate,
        uint128 _maxInterestRate,
        uint128 _currentInterestRate,
        uint128 _annualManagementFee,
        uint128 _minInterestRateChangePeriod
    ) external {
        _requireIsNotShutDown();
        _requireNonExistentInterestBatchManager(msg.sender);
        _requireValidAnnualInterestRate(_minInterestRate);
        _requireValidAnnualInterestRate(_maxInterestRate);
        // With the check below, it could only be ==
        if (_minInterestRate >= _maxInterestRate) revert MinGeMax();
        _requireInterestRateInRange(_currentInterestRate, _minInterestRate, _maxInterestRate);
        // Not needed, implicitly checked in the condition above:
        //_requireValidAnnualInterestRate(_currentInterestRate);
        if (_annualManagementFee > MAX_ANNUAL_BATCH_MANAGEMENT_FEE) revert AnnualManagementFeeTooHigh();
        if (_minInterestRateChangePeriod < MIN_INTEREST_RATE_CHANGE_PERIOD) revert MinInterestRateChangePeriodTooLow();

        interestBatchManagers[msg.sender] =
            InterestBatchManager(_minInterestRate, _maxInterestRate, _minInterestRateChangePeriod);

        troveManager.onRegisterBatchManager(msg.sender, _currentInterestRate, _annualManagementFee);
    }

    function lowerBatchManagementFee(uint256 _newAnnualManagementFee) external {
        _requireIsNotShutDown();
        _requireValidInterestBatchManager(msg.sender);

        ITroveManager troveManagerCached = troveManager;

        LatestBatchData memory batch = troveManagerCached.getLatestBatchData(msg.sender);
        if (_newAnnualManagementFee >= batch.annualManagementFee) {
            revert NewFeeNotLower();
        }

        // Lower batch fee on TM
        troveManagerCached.onLowerBatchManagerAnnualFee(
            msg.sender,
            batch.entireCollWithoutRedistribution,
            batch.entireDebtWithoutRedistribution,
            _newAnnualManagementFee
        );

        // active pool mint
        TroveChange memory batchChange;
        batchChange.batchAccruedManagementFee = batch.accruedManagementFee;
        batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt;
        batchChange.newWeightedRecordedDebt = batch.entireDebtWithoutRedistribution * batch.annualInterestRate;
        batchChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee;
        batchChange.newWeightedRecordedBatchManagementFee =
            batch.entireDebtWithoutRedistribution * _newAnnualManagementFee;

        activePool.mintAggInterestAndAccountForTroveChange(batchChange, msg.sender);
    }

    function setBatchManagerAnnualInterestRate(
        uint128 _newAnnualInterestRate,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _maxUpfrontFee
    ) external {
        _requireIsNotShutDown();
        _requireValidInterestBatchManager(msg.sender);
        _requireInterestRateInBatchManagerRange(msg.sender, _newAnnualInterestRate);
        // Not needed, implicitly checked in the condition above:
        //_requireValidAnnualInterestRate(_newAnnualInterestRate);

        ITroveManager troveManagerCached = troveManager;
        IActivePool activePoolCached = activePool;

        LatestBatchData memory batch = troveManagerCached.getLatestBatchData(msg.sender);
        _requireInterestRateChangePeriodPassed(msg.sender, uint256(batch.lastInterestRateAdjTime));

        uint256 newDebt = batch.entireDebtWithoutRedistribution;

        TroveChange memory batchChange;
        batchChange.batchAccruedManagementFee = batch.accruedManagementFee;
        batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt;
        batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate;
        batchChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee;
        batchChange.newWeightedRecordedBatchManagementFee = newDebt * batch.annualManagementFee;

        // Apply upfront fee on premature adjustments
        if (
            batch.annualInterestRate != _newAnnualInterestRate
                && block.timestamp < batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN
        ) {
            uint256 price = _requireOraclesLive();

            uint256 avgInterestRate = activePoolCached.getNewApproxAvgInterestRateFromTroveChange(batchChange);
            batchChange.upfrontFee = _calcUpfrontFee(newDebt, avgInterestRate);
            _requireUserAcceptsUpfrontFee(batchChange.upfrontFee, _maxUpfrontFee);

            newDebt += batchChange.upfrontFee;

            // Recalculate the batch's weighted terms, now taking into account the upfront fee
            batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate;
            batchChange.newWeightedRecordedBatchManagementFee = newDebt * batch.annualManagementFee;

            // Disallow a premature adjustment if it would result in TCR < CCR
            // (which includes the case when TCR is already below CCR before the adjustment).
            uint256 newTCR = _getNewTCRFromTroveChange(batchChange, price);
            _requireNewTCRisAboveCCR(newTCR);
        }

        activePoolCached.mintAggInterestAndAccountForTroveChange(batchChange, msg.sender);

        // Check batch is not empty, and then reinsert in sorted list
        if (!sortedTroves.isEmptyBatch(BatchId.wrap(msg.sender))) {
            sortedTroves.reInsertBatch(BatchId.wrap(msg.sender), _newAnnualInterestRate, _upperHint, _lowerHint);
        }

        troveManagerCached.onSetBatchManagerAnnualInterestRate(
            msg.sender, batch.entireCollWithoutRedistribution, newDebt, _newAnnualInterestRate
        );
    }

    function setInterestBatchManager(
        uint256 _troveId,
        address _newBatchManager,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _maxUpfrontFee
    ) public override {
        _requireIsNotShutDown();
        LocalVariables_setInterestBatchManager memory vars;
        vars.troveManager = troveManager;
        vars.activePool = activePool;
        vars.sortedTroves = sortedTroves;

        _requireTroveIsActive(vars.troveManager, _troveId);
        _requireCallerIsBorrower(_troveId);
        _requireValidInterestBatchManager(_newBatchManager);
        _requireIsNotInBatch(_troveId);

        interestBatchManagerOf[_troveId] = _newBatchManager;
        // Can’t have both individual delegation and batch manager
        if (interestIndividualDelegateOf[_troveId].account != address(0)) delete interestIndividualDelegateOf[_troveId];

        vars.trove = vars.troveManager.getLatestTroveData(_troveId);
        vars.newBatch = vars.troveManager.getLatestBatchData(_newBatchManager);

        TroveChange memory newBatchTroveChange;
        newBatchTroveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain;
        newBatchTroveChange.appliedRedistCollGain = vars.trove.redistCollGain;
        newBatchTroveChange.batchAccruedManagementFee = vars.newBatch.accruedManagementFee;
        newBatchTroveChange.oldWeightedRecordedDebt =
            vars.newBatch.weightedRecordedDebt + vars.trove.weightedRecordedDebt;
        newBatchTroveChange.newWeightedRecordedDebt =
            (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate;

        // We may check the old rate to see if it’s different than the new one, but then we should check the
        // last interest adjustment times to avoid gaming. So we decided to keep it simple and account it always
        // as a change. It’s probably not so common to join a batch with the exact same interest rate.
        // Apply upfront fee on premature adjustments
        if (block.timestamp < vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN) {
            vars.trove.entireDebt =
                _applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, newBatchTroveChange, _maxUpfrontFee);
        }

        // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee
        newBatchTroveChange.newWeightedRecordedDebt =
            (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate;

        // Add batch fees
        newBatchTroveChange.oldWeightedRecordedBatchManagementFee = vars.newBatch.weightedRecordedBatchManagementFee;
        newBatchTroveChange.newWeightedRecordedBatchManagementFee =
            (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualManagementFee;
        vars.activePool.mintAggInterestAndAccountForTroveChange(newBatchTroveChange, _newBatchManager);

        vars.troveManager.onSetInterestBatchManager(
            ITroveManager.OnSetInterestBatchManagerParams({
                troveId: _troveId,
                troveColl: vars.trove.entireColl,
                troveDebt: vars.trove.entireDebt,
                troveChange: newBatchTroveChange,
                newBatchAddress: _newBatchManager,
                newBatchColl: vars.newBatch.entireCollWithoutRedistribution,
                newBatchDebt: vars.newBatch.entireDebtWithoutRedistribution
            })
        );

        vars.sortedTroves.remove(_troveId);
        vars.sortedTroves.insertIntoBatch(
            _troveId, BatchId.wrap(_newBatchManager), vars.newBatch.annualInterestRate, _upperHint, _lowerHint
        );
    }

    function removeFromBatch(
        uint256 _troveId,
        uint256 _newAnnualInterestRate,
        uint256 _upperHint,
        uint256 _lowerHint,
        uint256 _maxUpfrontFee
    ) public override {
        _requireIsNotShutDown();

        LocalVariables_removeFromBatch memory vars;
        vars.troveManager = troveManager;
        vars.sortedTroves = sortedTroves;

        _requireTroveIsActive(vars.troveManager, _troveId);
        _requireCallerIsBorrower(_troveId);
        _requireValidAnnualInterestRate(_newAnnualInterestRate);

        vars.batchManager = _requireIsInBatch(_troveId);
        delete interestBatchManagerOf[_troveId];

        // Remove trove from Batch in SortedTroves
        vars.sortedTroves.removeFromBatch(_troveId);
        // Reinsert as single trove
        vars.sortedTroves.insert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint);

        vars.trove = vars.troveManager.getLatestTroveData(_troveId);
        vars.batch = vars.troveManager.getLatestBatchData(vars.batchManager);

        TroveChange memory batchChange;
        batchChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain;
        batchChange.appliedRedistCollGain = vars.trove.redistCollGain;
        batchChange.batchAccruedManagementFee = vars.batch.accruedManagementFee;
        batchChange.oldWeightedRecordedDebt = vars.batch.weightedRecordedDebt
            + (vars.trove.entireDebt - vars.trove.redistBoldDebtGain) * vars.batch.annualInterestRate;
        batchChange.newWeightedRecordedDebt = vars.batch.entireDebtWithoutRedistribution * vars.batch.annualInterestRate
            + vars.trove.entireDebt * _newAnnualInterestRate;

        // Apply upfront fee on premature adjustments
        if (
            vars.batch.annualInterestRate != _newAnnualInterestRate
            && block.timestamp < vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN
        ) {
            vars.trove.entireDebt =
                _applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, batchChange, _maxUpfrontFee);
        }

        // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee
        batchChange.newWeightedRecordedDebt = vars.batch.entireDebtWithoutRedistribution * vars.batch.annualInterestRate
            + vars.trove.entireDebt * _newAnnualInterestRate;
        // Add batch fees
        batchChange.oldWeightedRecordedBatchManagementFee = vars.batch.weightedRecordedBatchManagementFee
            + (vars.trove.entireDebt - batchChange.upfrontFee - vars.trove.redistBoldDebtGain)
                * vars.batch.annualManagementFee;
        batchChange.newWeightedRecordedBatchManagementFee =
            vars.batch.entireDebtWithoutRedistribution * vars.batch.annualManagementFee;

        activePool.mintAggInterestAndAccountForTroveChange(batchChange, vars.batchManager);

        vars.troveManager.onRemoveFromBatch(
            _troveId,
            vars.trove.entireColl,
            vars.trove.entireDebt,
            batchChange,
            vars.batchManager,
            vars.batch.entireCollWithoutRedistribution,
            vars.batch.entireDebtWithoutRedistribution,
            _newAnnualInterestRate
        );
    }

    function switchBatchManager(
        uint256 _troveId,
        uint256 _removeUpperHint,
        uint256 _removeLowerHint,
        address _newBatchManager,
        uint256 _addUpperHint,
        uint256 _addLowerHint,
        uint256 _maxUpfrontFee
    ) external override {
        address oldBatchManager = _requireIsInBatch(_troveId);
        _requireNewInterestBatchManager(oldBatchManager, _newBatchManager);

        LatestBatchData memory oldBatch = troveManager.getLatestBatchData(oldBatchManager);

        removeFromBatch(_troveId, oldBatch.annualInterestRate, _removeUpperHint, _removeLowerHint, 0);
        setInterestBatchManager(_troveId, _newBatchManager, _addUpperHint, _addLowerHint, _maxUpfrontFee);
    }

    function _applyUpfrontFee(
        uint256 _troveEntireColl,
        uint256 _troveEntireDebt,
        TroveChange memory _troveChange,
        uint256 _maxUpfrontFee
    ) internal returns (uint256) {
        uint256 price = _requireOraclesLive();

        uint256 avgInterestRate = activePool.getNewApproxAvgInterestRateFromTroveChange(_troveChange);
        _troveChange.upfrontFee = _calcUpfrontFee(_troveEntireDebt, avgInterestRate);
        _requireUserAcceptsUpfrontFee(_troveChange.upfrontFee, _maxUpfrontFee);

        _troveEntireDebt += _troveChange.upfrontFee;

        // ICR is based on the composite debt, i.e. the requested Bold amount + Bold gas comp + upfront fee.
        uint256 newICR = LiquityMath._computeCR(_troveEntireColl, _troveEntireDebt, price);
        _requireICRisAboveMCR(newICR);

        // Disallow a premature adjustment if it would result in TCR < CCR
        // (which includes the case when TCR is already below CCR before the adjustment).
        uint256 newTCR = _getNewTCRFromTroveChange(_troveChange, price);
        _requireNewTCRisAboveCCR(newTCR);

        return _troveEntireDebt;
    }

    function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal pure returns (uint256) {
        return _calcInterest(_debt * _avgInterestRate, UPFRONT_INTEREST_PERIOD);
    }

    // Call from TM to clean state here
    function onLiquidateTrove(uint256 _troveId) external {
        _requireCallerIsTroveManager();

        _wipeTroveMappings(_troveId);
    }

    function _wipeTroveMappings(uint256 _troveId) internal {
        delete interestIndividualDelegateOf[_troveId];
        delete interestBatchManagerOf[_troveId];
        delete addManagerOf[_troveId];
        delete removeManagerReceiverOf[_troveId];
    }

    /**
     * Claim remaining collateral from a liquidation with ICR exceeding the liquidation penalty
     */
    function claimCollateral() external override {
        // send coll from CollSurplus Pool to owner
        collSurplusPool.claimColl(msg.sender);
    }

    function shutdown() external {
        if (hasBeenShutDown) revert IsShutDown();

        uint256 totalColl = getEntireSystemColl();
        uint256 totalDebt = getEntireSystemDebt();
        (uint256 price, ) = priceFeed.fetchPrice();

        uint256 TCR = LiquityMath._computeCR(totalColl, totalDebt, price);
        if (TCR >= SCR) revert TCRNotBelowSCR();

        _applyShutdown();

        emit ShutDown(TCR);
    }

    // Not technically a "Borrower op", but seems best placed here given current shutdown logic.
    function shutdownFromOracleFailure(address _failedOracleAddr) external {
        _requireCallerIsPriceFeed();

        // No-op rather than revert here, so that the outer function call which fetches the price does not revert
        // if the system is already shut down.
        if (hasBeenShutDown) return;

        _applyShutdown();

        emit ShutDownFromOracleFailure(_failedOracleAddr);
    }

    function _applyShutdown() internal {
        activePool.mintAggInterest();
        hasBeenShutDown = true;
        troveManager.shutdown();
    }

    // --- Helper functions ---

    function _reInsertIntoSortedTroves(
        uint256 _troveId,
        uint256 _troveAnnualInterestRate,
        uint256 _upperHint,
        uint256 _lowerHint,
        address _batchManager,
        uint256 _batchAnnualInterestRate
    ) internal {
        // If it was in a batch, we need to put it back, otherwise we insert it normally
        if (_batchManager == address(0)) {
            sortedTroves.insert(_troveId, _troveAnnualInterestRate, _upperHint, _lowerHint);
        } else {
            sortedTroves.insertIntoBatch(
                _troveId, BatchId.wrap(_batchManager), _batchAnnualInterestRate, _upperHint, _lowerHint
            );
        }
    }

    // This function mints the BOLD corresponding to the borrower's chosen debt increase
    // (it does not mint the accrued interest).
    function _moveTokensFromAdjustment(
        address withdrawalReceiver,
        TroveChange memory _troveChange,
        IBoldToken _boldToken,
        IActivePool _activePool
    ) internal {
        if (_troveChange.debtIncrease > 0) {
            _boldToken.mint(withdrawalReceiver, _troveChange.debtIncrease);
        } else if (_troveChange.debtDecrease > 0) {
            _boldToken.burn(msg.sender, _troveChange.debtDecrease);
        }

        if (_troveChange.collIncrease > 0) {
            // Pull coll tokens from sender and move them to the Active Pool
            _pullCollAndSendToActivePool(_activePool, _troveChange.collIncrease);
        } else if (_troveChange.collDecrease > 0) {
            // Pull Coll from Active Pool and decrease its recorded Coll balance
            _activePool.sendColl(withdrawalReceiver, _troveChange.collDecrease);
        }
    }

    function _pullCollAndSendToActivePool(IActivePool _activePool, uint256 _amount) internal {
        // Send Coll tokens from sender to active pool
        collToken.safeTransferFrom(msg.sender, address(_activePool), _amount);
        // Make sure Active Pool accountancy is right
        _activePool.accountForReceivedColl(_amount);
    }

    function checkBatchManagerExists(address _batchManager) external view returns (bool) {
        return interestBatchManagers[_batchManager].maxInterestRate > 0;
    }

    // --- 'Require' wrapper functions ---

    function _requireIsNotShutDown() internal view {
        if (hasBeenShutDown) {
            revert IsShutDown();
        }
    }

    function _requireIsShutDown() internal view {
        if (!hasBeenShutDown) {
            revert NotShutDown();
        }
    }

    function _requireNonZeroAdjustment(TroveChange memory _troveChange) internal pure {
        if (
            _troveChange.collIncrease == 0 && _troveChange.collDecrease == 0 && _troveChange.debtIncrease == 0
                && _troveChange.debtDecrease == 0
        ) {
            revert ZeroAdjustment();
        }
    }

    function _requireSenderIsOwnerOrInterestManager(uint256 _troveId, address _owner) internal view {
        if (msg.sender != _owner && msg.sender != interestIndividualDelegateOf[_troveId].account) {
            revert NotOwnerNorInterestManager();
        }
    }

    function _requireIsNotInBatch(uint256 _troveId) internal view {
        if (interestBatchManagerOf[_troveId] != address(0)) {
            revert TroveInBatch();
        }
    }

    function _requireIsInBatch(uint256 _troveId) internal view returns (address) {
        address batchManager = interestBatchManagerOf[_troveId];
        if (batchManager == address(0)) {
            revert TroveNotInBatch();
        }

        return batchManager;
    }

    function _requireInterestRateInDelegateRange(uint256 _troveId, uint256 _annualInterestRate) internal view {
        InterestIndividualDelegate memory individualDelegate = interestIndividualDelegateOf[_troveId];
        if (individualDelegate.account != address(0)) {
            _requireInterestRateInRange(
                _annualInterestRate, individualDelegate.minInterestRate, individualDelegate.maxInterestRate
            );
        }
    }

    function _requireInterestRateInBatchManagerRange(address _interestBatchManagerAddress, uint256 _annualInterestRate)
        internal
        view
    {
        InterestBatchManager memory interestBatchManager = interestBatchManagers[_interestBatchManagerAddress];
        _requireInterestRateInRange(
            _annualInterestRate, interestBatchManager.minInterestRate, interestBatchManager.maxInterestRate
        );
    }

    function _requireInterestRateInRange(
        uint256 _annualInterestRate,
        uint256 _minInterestRate,
        uint256 _maxInterestRate
    ) internal pure {
        if (_minInterestRate > _annualInterestRate || _annualInterestRate > _maxInterestRate) {
            revert InterestNotInRange();
        }
    }

    function _requireInterestRateChangePeriodPassed(
        address _interestBatchManagerAddress,
        uint256 _lastInterestRateAdjTime
    ) internal view {
        InterestBatchManager memory interestBatchManager = interestBatchManagers[_interestBatchManagerAddress];
        if (block.timestamp < _lastInterestRateAdjTime + uint256(interestBatchManager.minInterestRateChangePeriod)) {
            revert BatchInterestRateChangePeriodNotPassed();
        }
    }

    function _requireTroveIsOpen(ITroveManager _troveManager, uint256 _troveId) internal view {
        ITroveManager.Status status = _troveManager.getTroveStatus(_troveId);
        if (status != ITroveManager.Status.active && status != ITroveManager.Status.unredeemable) {
            revert TroveNotOpen();
        }
    }

    function _requireTroveIsActive(ITroveManager _troveManager, uint256 _troveId) internal view {
        ITroveManager.Status status = _troveManager.getTroveStatus(_troveId);
        if (status != ITroveManager.Status.active) {
            revert TroveNotActive();
        }
    }

    function _requireTroveIsUnredeemable(ITroveManager _troveManager, uint256 _troveId) internal view {
        if (!_checkTroveIsUnredeemable(_troveManager, _troveId)) {
            revert TroveNotUnredeemable();
        }
    }

    function _checkTroveIsUnredeemable(ITroveManager _troveManager, uint256 _troveId) internal view returns (bool) {
        ITroveManager.Status status = _troveManager.getTroveStatus(_troveId);
        return status == ITroveManager.Status.unredeemable;
    }

    function _requireTroveIsNotOpen(ITroveManager _troveManager, uint256 _troveId) internal view {
        ITroveManager.Status status = _troveManager.getTroveStatus(_troveId);
        if (status == ITroveManager.Status.active || status == ITroveManager.Status.unredeemable) {
            revert TroveOpen();
        }
    }

    function _requireUserAcceptsUpfrontFee(uint256 _fee, uint256 _maxFee) internal pure {
        if (_fee > _maxFee) {
            revert UpfrontFeeTooHigh();
        }
    }

    function _requireNotBelowCriticalThreshold(uint256 _price) internal view {
        if (_checkBelowCriticalThreshold(_price, CCR)) {
            revert BelowCriticalThreshold();
        }
    }

    function _requireNoBorrowing(uint256 _debtIncrease) internal pure {
        if (_debtIncrease > 0) {
            revert BorrowingNotPermittedBelowCT();
        }
    }

    function _requireValidAdjustmentInCurrentMode(
        TroveChange memory _troveChange,
        LocalVariables_adjustTrove memory _vars
    ) internal view {
        /*
        * Below Critical Threshold, it is not permitted:
        *
        * - Borrowing
        * - Collateral withdrawal except accompanied by a debt repayment of at least the same value
        *
        * In Normal Mode, ensure:
        *
        * - The adjustment won't pull the TCR below CCR
        *
        * In Both cases:
        * - The new ICR is above MCR
        */
        _requireICRisAboveMCR(_vars.newICR);

        if (_vars.isBelowCriticalThreshold) {
            _requireNoBorrowing(_troveChange.debtIncrease);
            _requireDebtRepaymentGeCollWithdrawal(_troveChange, _vars.price);
        } else {
            // if Normal Mode
            uint256 newTCR = _getNewTCRFromTroveChange(_troveChange, _vars.price);
            _requireNewTCRisAboveCCR(newTCR);
        }
    }

    function _requireICRisAboveMCR(uint256 _newICR) internal view {
        if (_newICR < MCR) {
            revert ICRBelowMCR();
        }
    }

    function _requireDebtRepaymentGeCollWithdrawal(TroveChange memory _troveChange, uint256 _price) internal pure {
        if ((_troveChange.debtDecrease * DECIMAL_PRECISION < _troveChange.collDecrease * _price)) {
            revert RepaymentNotMatchingCollWithdrawal();
        }
    }

    function _requireNewTCRisAboveCCR(uint256 _newTCR) internal view {
        if (_newTCR < CCR) {
            revert TCRBelowCCR();
        }
    }

    function _requireAtLeastMinDebt(uint256 _debt) internal pure {
        if (_debt < MIN_DEBT) {
            revert DebtBelowMin();
        }
    }

    function _requireValidCollWithdrawal(uint256 _currentColl, uint256 _collWithdrawal) internal pure {
        if (_collWithdrawal > _currentColl) {
            revert CollWithdrawalTooHigh();
        }
    }

    function _requireSufficientBoldBalance(IBoldToken _boldToken, address _borrower, uint256 _debtRepayment)
        internal
        view
    {
        if (_boldToken.balanceOf(_borrower) < _debtRepayment) {
            revert NotEnoughBoldBalance();
        }
    }

    function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal pure {
        if (_annualInterestRate < MIN_ANNUAL_INTEREST_RATE) {
            revert InterestRateTooLow();
        }
        if (_annualInterestRate > MAX_ANNUAL_INTEREST_RATE) {
            revert InterestRateTooHigh();
        }
    }

    function _requireAnnualInterestRateIsNew(uint256 _oldAnnualInterestRate, uint256 _newAnnualInterestRate)
        internal
        pure
    {
        if (_oldAnnualInterestRate == _newAnnualInterestRate) {
            revert InterestRateNotNew();
        }
    }

    function _requireValidInterestBatchManager(address _interestBatchManagerAddress) internal view {
        if (interestBatchManagers[_interestBatchManagerAddress].maxInterestRate == 0) {
            revert InvalidInterestBatchManager();
        }
    }

    function _requireNonExistentInterestBatchManager(address _interestBatchManagerAddress) internal view {
        if (interestBatchManagers[_interestBatchManagerAddress].maxInterestRate > 0) {
            revert BatchManagerExists();
        }
    }

    function _requireNewInterestBatchManager(address _oldBatchManagerAddress, address _newBatchManagerAddress)
        internal
        pure
    {
        if (_oldBatchManagerAddress == _newBatchManagerAddress) {
            revert BatchManagerNotNew();
        }
    }

    function _requireCallerIsTroveManager() internal view {
        if (msg.sender != address(troveManager)) {
            revert CallerNotTroveManager();
        }
    }

    function _requireCallerIsPriceFeed() internal view {
        if (msg.sender != address(priceFeed)) {
            revert CallerNotPriceFeed();
        }
    }

    function _requireOraclesLive() internal returns (uint256) {
        (uint256 price, bool newOracleFailureDetected) = priceFeed.fetchPrice();
        if (newOracleFailureDetected) {
            revert NewOracleFailureDetected();
        }

        return price;
    }

    // --- ICR and TCR getters ---

    function _getNewTCRFromTroveChange(TroveChange memory _troveChange, uint256 _price)
        internal
        view
        returns (uint256 newTCR)
    {
        uint256 totalColl = getEntireSystemColl();
        totalColl += _troveChange.collIncrease;
        totalColl -= _troveChange.collDecrease;

        uint256 totalDebt = getEntireSystemDebt();
        totalDebt += _troveChange.debtIncrease;
        totalDebt += _troveChange.upfrontFee;
        totalDebt -= _troveChange.debtDecrease;

        newTCR = LiquityMath._computeCR(totalColl, totalDebt, _price);
    }
}


Vulnerability type: CWE-435