LoopFi

[WP-C1] positions[owner].debt can be reset to 0 through CDPVault.modifyCollateralAndDebt({ deltaDebt: 0, ... })

When deltaDebt == 0 in CDPVault.modifyCollateralAndDebt(), it's expected not to change the debt state.

Current Implementation

Since deltaDebt == 0 satisfies neither deltaDebt > 0 nor deltaDebt < 0, calling CDPVault.modifyCollateralAndDebt({ deltaDebt: 0, ... }) does not require special permission.

When execution reaches L421, newDebt and newCumulativeIndex are initialized to their default value 0.

This will reset the positions[owner].debt to 0.

@@ 343,356 @@ /// @notice Modifies a Position's collateral and debt balances /// @dev Checks that the global debt ceiling and the vault's debt ceiling have not been exceeded via the CDM, /// - that the Position is still safe after the modification, /// - that the msg.sender has the permission of the owner to decrease the collateral-to-debt ratio, /// - that the msg.sender has the permission of the collateralizer to put up new collateral, /// - that the msg.sender has the permission of the creditor to settle debt with their credit, /// - that that the vault debt floor is exceeded /// - that the vault minimum collateralization ratio is met /// @param owner Address of the owner of the position /// @param collateralizer Address of who puts up or receives the collateral delta /// @param creditor Address of who provides or receives the credit delta for the debt delta /// @param deltaCollateral Amount of collateral to put up (+) or to remove (-) from the position [wad] /// @param deltaDebt Amount of normalized debt (gross, before rate is applied) to generate (+) or /// to settle (-) on this position [wad]
function modifyCollateralAndDebt( address owner, address collateralizer, address creditor, int256 deltaCollateral, int256 deltaDebt ) public { if ( // position is either more safe than before or msg.sender has the permission from the owner ((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) || // msg.sender has the permission of the collateralizer to collateralize the position using their cash (deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) || // msg.sender has the permission of the creditor to use their credit to repay the debt (deltaDebt < 0 && !hasPermission(creditor, msg.sender)) ) revert CDPVault__modifyCollateralAndDebt_noPermission(); Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); uint256 newDebt; uint256 newCumulativeIndex; uint256 profit; if (deltaDebt > 0) { (newDebt, newCumulativeIndex) = calcIncrease( uint256(deltaDebt), // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); // U:[CM-10] pool.lendCreditAccount(uint256(deltaDebt), creditor); // F:[CM-20] } else if (deltaDebt < 0) {
@@ 389,410 @@ uint256 maxRepayment = calcTotalDebt(debtData); uint256 amount = abs(deltaDebt); if (amount >= maxRepayment) { amount = maxRepayment; // U:[CM-11] } poolUnderlying.safeTransferFrom(creditor, address(pool), amount); if (amount == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( amount, // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11]
} if (deltaCollateral > 0) { uint256 amount = deltaCollateral.toUint256(); token.safeTransferFrom(collateralizer, address(this), amount); } else if (deltaCollateral < 0) { uint256 amount = abs(deltaCollateral); token.safeTransfer(collateralizer, amount); } position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt); VaultConfig memory config = vaultConfig; uint256 spotPrice_ = spotPrice(); uint256 collateralValue = wmul(position.collateral, spotPrice_); if ( (deltaDebt > 0 || deltaCollateral < 0) && !_isCollateralized(newDebt, collateralValue, config.liquidationRatio) ) revert CDPVault__modifyCollateralAndDebt_notSafe(); emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt); }

CDPVault.sol

    /// @notice Updates a position's collateral and debt balances
    /// @dev This is the only method which is allowed to modify a position's collateral and debt balances
    function _modifyPosition(
        address owner,
        Position memory position,
        uint256 newDebt,
        uint256 newCumulativeIndex,
        int256 deltaCollateral,
        uint256 totalDebt_
    ) internal returns (Position memory) {
        uint256 currentDebt = position.debt;
        // update collateral and debt amounts by the deltas
        position.collateral = add(position.collateral, deltaCollateral);
        position.debt = newDebt; // U:[CM-10,11]
        position.cumulativeIndexLastUpdate = newCumulativeIndex; // U:[CM-10,11]
        position.lastDebtUpdate = uint64(block.number); // U:[CM-10,11]

        // position either has no debt or more debt than the debt floor
        if (position.debt != 0 && position.debt < uint256(vaultConfig.debtFloor))
            revert CDPVault__modifyPosition_debtFloor();

        // store the position's balances
        positions[owner] = position;

        // update the global debt balance
        if (newDebt > currentDebt) {
            totalDebt_ = totalDebt_ + (newDebt - currentDebt);
        } else {
            totalDebt_ = totalDebt_ - (currentDebt - newDebt);
        }
        totalDebt = totalDebt_;

        if (address(rewardController) != address(0)) {
            rewardController.handleActionAfter(owner, position.debt, totalDebt_);
        }

        emit ModifyPosition(owner, position.debt, position.collateral, totalDebt_);

        return position;
    }
@@ 343,356 @@ /// @notice Modifies a Position's collateral and debt balances /// @dev Checks that the global debt ceiling and the vault's debt ceiling have not been exceeded via the CDM, /// - that the Position is still safe after the modification, /// - that the msg.sender has the permission of the owner to decrease the collateral-to-debt ratio, /// - that the msg.sender has the permission of the collateralizer to put up new collateral, /// - that the msg.sender has the permission of the creditor to settle debt with their credit, /// - that that the vault debt floor is exceeded /// - that the vault minimum collateralization ratio is met /// @param owner Address of the owner of the position /// @param collateralizer Address of who puts up or receives the collateral delta /// @param creditor Address of who provides or receives the credit delta for the debt delta /// @param deltaCollateral Amount of collateral to put up (+) or to remove (-) from the position [wad] /// @param deltaDebt Amount of normalized debt (gross, before rate is applied) to generate (+) or /// to settle (-) on this position [wad]
function modifyCollateralAndDebt( address owner, address collateralizer, address creditor, int256 deltaCollateral, int256 deltaDebt ) public { if ( // position is either more safe than before or msg.sender has the permission from the owner ((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) || // msg.sender has the permission of the collateralizer to collateralize the position using their cash (deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) || // msg.sender has the permission of the creditor to use their credit to repay the debt (deltaDebt < 0 && !hasPermission(creditor, msg.sender)) ) revert CDPVault__modifyCollateralAndDebt_noPermission(); Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); uint256 newDebt; uint256 newCumulativeIndex; uint256 profit; if (deltaDebt > 0) { (newDebt, newCumulativeIndex) = calcIncrease( uint256(deltaDebt), // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); // U:[CM-10] pool.lendCreditAccount(uint256(deltaDebt), creditor); // F:[CM-20] } else if (deltaDebt < 0) {
@@ 389,410 @@ uint256 maxRepayment = calcTotalDebt(debtData); uint256 amount = abs(deltaDebt); if (amount >= maxRepayment) { amount = maxRepayment; // U:[CM-11] } poolUnderlying.safeTransferFrom(creditor, address(pool), amount); if (amount == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( amount, // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11]
} if (deltaCollateral > 0) { uint256 amount = deltaCollateral.toUint256(); token.safeTransferFrom(collateralizer, address(this), amount); } else if (deltaCollateral < 0) { uint256 amount = abs(deltaCollateral); token.safeTransfer(collateralizer, amount); } position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt); VaultConfig memory config = vaultConfig; uint256 spotPrice_ = spotPrice(); uint256 collateralValue = wmul(position.collateral, spotPrice_); if ( (deltaDebt > 0 || deltaCollateral < 0) && !_isCollateralized(newDebt, collateralValue, config.liquidationRatio) ) revert CDPVault__modifyCollateralAndDebt_notSafe(); emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt); }

[WP-C2] The attacker can construct flashloan parameters to take away the borrowed money from the position.

Based on the context, we assume that minter.exit() is the method to mint stablecoins.

The expected behavior is to use the flashloan to swap for collateral, and then borrow stablecoin to repay the flashloan. However, at PositionAction.sol#L455, the minter repays the flashloan, causing the borrowed funds from the expanded debt to be left in the proxy contract.

Since the flashloan parameters can be arbitrarily constructed, an attacker can create fake data (with a fake Vault contract) and back-run a normal flashloan user.

At PositionAction20.sol#L67, the attacker's fake Vault can obtain the allowance for this amount and transfer the funds away (with ICDPVault(leverParams.vault).deposit()).

function flashLoan(
    IERC3156FlashBorrower receiver,
    address token,
    uint256 amount,
    bytes calldata data
) external override nonReentrant returns (bool) {
    if (token != address(stablecoin)) revert Flash__flashLoan_unsupportedToken();
    uint256 fee = wmul(amount, protocolFee);
    uint256 total = amount + fee;

    minter.exit(address(receiver), amount);

    emit FlashLoan(address(receiver), token, amount, fee);

    if (receiver.onFlashLoan(msg.sender, token, amount, fee, data) != CALLBACK_SUCCESS)
        revert Flash__flashLoan_callbackFailed();

    // reverts if not enough Stablecoin have been send back
    stablecoin.transferFrom(address(receiver), address(this), total);
    minter.enter(address(this), total);

    return true;
}
 function onFlashLoan(
@@ 411,415 @@ address /*initiator*/, address /*token*/, uint256 /*amount*/, uint256 /*fee*/, bytes calldata data
) external returns (bytes32) { if (msg.sender != address(flashlender)) revert PositionAction__onFlashLoan__invalidSender(); (LeverParams memory leverParams, address upFrontToken, uint256 upFrontAmount) = abi.decode( data, (LeverParams, address, uint256) );
@@ 422,438 @@ // perform a pre swap from arbitrary token to collateral token if necessary if (leverParams.auxSwap.assetIn != address(0)) { bytes memory auxSwapData = _delegateCall( address(swapAction), abi.encodeWithSelector(swapAction.swap.selector, leverParams.auxSwap) ); upFrontAmount = abi.decode(auxSwapData, (uint256)); } // swap stablecoin to collateral bytes memory swapData = _delegateCall( address(swapAction), abi.encodeWithSelector(swapAction.swap.selector, leverParams.primarySwap) ); uint256 swapAmountOut = abi.decode(swapData, (uint256));
// deposit collateral and handle any CDP specific actions uint256 collateral = _onIncreaseLever(leverParams, upFrontToken, upFrontAmount, swapAmountOut); // derive the amount of normal debt from the amount of Stablecoin swapped uint256 addNormalDebt = _debtToNormalDebt(leverParams.vault, leverParams.primarySwap.amount); // add collateral and debt ICDPVault(leverParams.vault).modifyCollateralAndDebt( leverParams.position, address(this), address(this), toInt256(collateral), toInt256(addNormalDebt) ); // mint stablecoin to pay back the flash loans minter.exit(address(this), leverParams.primarySwap.amount); // Approve stablecoin to be used to pay back the flash loan. stablecoin.approve(address(flashlender), leverParams.primarySwap.amount); return CALLBACK_SUCCESS; }

PositionAction20.sol

function _onIncreaseLever(
    LeverParams memory leverParams,
    address /*upFrontToken*/,
    uint256 upFrontAmount,
    uint256 swapAmountOut
) internal override returns (uint256) {
    // for standard erc20 cdps treat the upFrontAmount and swapAmountOut as the collateral token
    uint256 addCollateralAmount = swapAmountOut + upFrontAmount;

    // deposit into the CDP Vault
    IERC20(leverParams.collateralToken).forceApprove(leverParams.vault, addCollateralAmount);
    return ICDPVault(leverParams.vault).deposit(address(this), addCollateralAmount);
}
function flashLoan(
    IERC3156FlashBorrower receiver,
    address token,
    uint256 amount,
    bytes calldata data
) external override nonReentrant returns (bool) {
    if (token != address(stablecoin)) revert Flash__flashLoan_unsupportedToken();
    uint256 fee = wmul(amount, protocolFee);
    uint256 total = amount + fee;

    minter.exit(address(receiver), amount);

    emit FlashLoan(address(receiver), token, amount, fee);

    if (receiver.onFlashLoan(msg.sender, token, amount, fee, data) != CALLBACK_SUCCESS)
        revert Flash__flashLoan_callbackFailed();

    // reverts if not enough Stablecoin have been send back
    stablecoin.transferFrom(address(receiver), address(this), total);
    minter.enter(address(this), total);

    return true;
}
 function onFlashLoan(
@@ 411,415 @@ address /*initiator*/, address /*token*/, uint256 /*amount*/, uint256 /*fee*/, bytes calldata data
) external returns (bytes32) { if (msg.sender != address(flashlender)) revert PositionAction__onFlashLoan__invalidSender(); (LeverParams memory leverParams, address upFrontToken, uint256 upFrontAmount) = abi.decode( data, (LeverParams, address, uint256) );
@@ 422,438 @@ // perform a pre swap from arbitrary token to collateral token if necessary if (leverParams.auxSwap.assetIn != address(0)) { bytes memory auxSwapData = _delegateCall( address(swapAction), abi.encodeWithSelector(swapAction.swap.selector, leverParams.auxSwap) ); upFrontAmount = abi.decode(auxSwapData, (uint256)); } // swap stablecoin to collateral bytes memory swapData = _delegateCall( address(swapAction), abi.encodeWithSelector(swapAction.swap.selector, leverParams.primarySwap) ); uint256 swapAmountOut = abi.decode(swapData, (uint256));
// deposit collateral and handle any CDP specific actions uint256 collateral = _onIncreaseLever(leverParams, upFrontToken, upFrontAmount, swapAmountOut); // derive the amount of normal debt from the amount of Stablecoin swapped uint256 addNormalDebt = _debtToNormalDebt(leverParams.vault, leverParams.primarySwap.amount); // add collateral and debt ICDPVault(leverParams.vault).modifyCollateralAndDebt( leverParams.position, address(this), address(this), toInt256(collateral), toInt256(addNormalDebt) ); // mint stablecoin to pay back the flash loans minter.exit(address(this), leverParams.primarySwap.amount); // Approve stablecoin to be used to pay back the flash loan. stablecoin.approve(address(flashlender), leverParams.primarySwap.amount); return CALLBACK_SUCCESS; }

[WP-H3] Health Factor Calculation Should Take Interest and Fees Into Account

  1. Positions that are already insolvent due to accumulated interest cannot be liquidated in a timely manner.
  2. Users can borrow more funds than they should be allowed.

Recommendation

It is recommended to use the calcTotalDebt() method when calculating the health factor.

 function liquidatePosition(address owner, uint256 repayAmount) external whenNotPaused {
        // validate params
        if (owner == address(0) || repayAmount == 0) revert CDPVault__liquidatePosition_invalidParameters();

        // load configs
        VaultConfig memory config = vaultConfig;
        LiquidationConfig memory liqConfig_ = liquidationConfig;

        // load liquidated position
        Position memory position = positions[owner];
        DebtData memory debtData = _calcDebt(position);

        // load price and calculate discounted price
        uint256 spotPrice_ = spotPrice();
        uint256 discountedPrice = wmul(spotPrice_, liqConfig_.liquidationDiscount);
        if (spotPrice_ == 0) revert CDPVault__liquidatePosition_invalidSpotPrice();

        // compute collateral to take, debt to repay and penalty to pay
        uint256 takeCollateral = wdiv(repayAmount, discountedPrice);
        uint256 deltaDebt = wmul(repayAmount, liqConfig_.liquidationPenalty);
        uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty);

       

        // verify that the position is indeed unsafe
        if (_isCollateralized(debtData.debt, wmul(position.collateral, spotPrice_), config.liquidationRatio))
            revert CDPVault__liquidatePosition_notUnsafe();

@@ 504,549 @@ // account for bad debt // TODO: review this if (takeCollateral > position.collateral) { takeCollateral = position.collateral; repayAmount = wmul(takeCollateral, discountedPrice); penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty); // debt >= repayAmount if takeCollateral > position.collateral //deltaDebt = currentDebt; deltaDebt = debtData.debt; } // update vault state totalDebt -= deltaDebt; // transfer the repay amount from the liquidator to the vault poolUnderlying.safeTransferFrom(msg.sender, address(pool), deltaDebt); uint256 newDebt; uint256 profit; uint256 maxRepayment = calcTotalDebt(debtData); { uint256 newCumulativeIndex; if (deltaDebt == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( deltaDebt, // delta debt debtData.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray debtData.cumulativeIndexLastUpdate ); } // update liquidated position position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, -toInt256(takeCollateral), totalDebt); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] // transfer the collateral amount from the vault to the liquidator // cash[msg.sender] += takeCollateral; token.safeTransfer(msg.sender, takeCollateral); // Mint the penalty from the vault to the treasury // cdm.modifyBalance(address(this), address(buffer), penalty); IPoolV3Loop(address(pool)).mintProfit(penalty);
}
function _calcDebt(Position memory position) internal view returns (DebtData memory cdd) {
    uint256 index = pool.baseInterestIndex();
    cdd.debt = position.debt;
    cdd.cumulativeIndexNow = index;
    cdd.cumulativeIndexLastUpdate = position.cumulativeIndexLastUpdate;

    cdd.accruedInterest = calcAccruedInterest(cdd.debt, cdd.cumulativeIndexLastUpdate, index);

    cdd.accruedFees = (cdd.accruedInterest * feeInterest) / PERCENTAGE_FACTOR;
}
    function modifyCollateralAndDebt(
        address owner,
        address collateralizer,
        address creditor,
        int256 deltaCollateral,
        int256 deltaDebt
    ) public {
@@ 364,426 @@ if ( // position is either more safe than before or msg.sender has the permission from the owner ((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) || // msg.sender has the permission of the collateralizer to collateralize the position using their cash (deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) || // msg.sender has the permission of the creditor to use their credit to repay the debt (deltaDebt < 0 && !hasPermission(creditor, msg.sender)) ) revert CDPVault__modifyCollateralAndDebt_noPermission(); Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); uint256 newDebt; uint256 newCumulativeIndex; uint256 profit; if (deltaDebt > 0) { (newDebt, newCumulativeIndex) = calcIncrease( uint256(deltaDebt), // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); // U:[CM-10] pool.lendCreditAccount(uint256(deltaDebt), creditor); // F:[CM-20] } else if (deltaDebt < 0) { uint256 maxRepayment = calcTotalDebt(debtData); uint256 amount = abs(deltaDebt); if (amount >= maxRepayment) { amount = maxRepayment; // U:[CM-11] } poolUnderlying.safeTransferFrom(creditor, address(pool), amount); if (amount == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( amount, // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] } if (deltaCollateral > 0) { uint256 amount = deltaCollateral.toUint256(); token.safeTransferFrom(collateralizer, address(this), amount); } else if (deltaCollateral < 0) { uint256 amount = abs(deltaCollateral); token.safeTransfer(collateralizer, amount); } position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt); VaultConfig memory config = vaultConfig; uint256 spotPrice_ = spotPrice(); uint256 collateralValue = wmul(position.collateral, spotPrice_);
if ( (deltaDebt > 0 || deltaCollateral < 0) && !_isCollateralized(newDebt, collateralValue, config.liquidationRatio) ) revert CDPVault__modifyCollateralAndDebt_notSafe(); emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt); }
 function liquidatePosition(address owner, uint256 repayAmount) external whenNotPaused {
        // validate params
        if (owner == address(0) || repayAmount == 0) revert CDPVault__liquidatePosition_invalidParameters();

        // load configs
        VaultConfig memory config = vaultConfig;
        LiquidationConfig memory liqConfig_ = liquidationConfig;

        // load liquidated position
        Position memory position = positions[owner];
        DebtData memory debtData = _calcDebt(position);

        // load price and calculate discounted price
        uint256 spotPrice_ = spotPrice();
        uint256 discountedPrice = wmul(spotPrice_, liqConfig_.liquidationDiscount);
        if (spotPrice_ == 0) revert CDPVault__liquidatePosition_invalidSpotPrice();

        // compute collateral to take, debt to repay and penalty to pay
        uint256 takeCollateral = wdiv(repayAmount, discountedPrice);
        uint256 deltaDebt = wmul(repayAmount, liqConfig_.liquidationPenalty);
        uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty);

       

        // verify that the position is indeed unsafe
        if (_isCollateralized(debtData.debt, wmul(position.collateral, spotPrice_), config.liquidationRatio))
            revert CDPVault__liquidatePosition_notUnsafe();

@@ 504,549 @@ // account for bad debt // TODO: review this if (takeCollateral > position.collateral) { takeCollateral = position.collateral; repayAmount = wmul(takeCollateral, discountedPrice); penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty); // debt >= repayAmount if takeCollateral > position.collateral //deltaDebt = currentDebt; deltaDebt = debtData.debt; } // update vault state totalDebt -= deltaDebt; // transfer the repay amount from the liquidator to the vault poolUnderlying.safeTransferFrom(msg.sender, address(pool), deltaDebt); uint256 newDebt; uint256 profit; uint256 maxRepayment = calcTotalDebt(debtData); { uint256 newCumulativeIndex; if (deltaDebt == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( deltaDebt, // delta debt debtData.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray debtData.cumulativeIndexLastUpdate ); } // update liquidated position position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, -toInt256(takeCollateral), totalDebt); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] // transfer the collateral amount from the vault to the liquidator // cash[msg.sender] += takeCollateral; token.safeTransfer(msg.sender, takeCollateral); // Mint the penalty from the vault to the treasury // cdm.modifyBalance(address(this), address(buffer), penalty); IPoolV3Loop(address(pool)).mintProfit(penalty);
}
function _calcDebt(Position memory position) internal view returns (DebtData memory cdd) {
    uint256 index = pool.baseInterestIndex();
    cdd.debt = position.debt;
    cdd.cumulativeIndexNow = index;
    cdd.cumulativeIndexLastUpdate = position.cumulativeIndexLastUpdate;

    cdd.accruedInterest = calcAccruedInterest(cdd.debt, cdd.cumulativeIndexLastUpdate, index);

    cdd.accruedFees = (cdd.accruedInterest * feeInterest) / PERCENTAGE_FACTOR;
}
    function modifyCollateralAndDebt(
        address owner,
        address collateralizer,
        address creditor,
        int256 deltaCollateral,
        int256 deltaDebt
    ) public {
@@ 364,426 @@ if ( // position is either more safe than before or msg.sender has the permission from the owner ((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) || // msg.sender has the permission of the collateralizer to collateralize the position using their cash (deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) || // msg.sender has the permission of the creditor to use their credit to repay the debt (deltaDebt < 0 && !hasPermission(creditor, msg.sender)) ) revert CDPVault__modifyCollateralAndDebt_noPermission(); Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); uint256 newDebt; uint256 newCumulativeIndex; uint256 profit; if (deltaDebt > 0) { (newDebt, newCumulativeIndex) = calcIncrease( uint256(deltaDebt), // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); // U:[CM-10] pool.lendCreditAccount(uint256(deltaDebt), creditor); // F:[CM-20] } else if (deltaDebt < 0) { uint256 maxRepayment = calcTotalDebt(debtData); uint256 amount = abs(deltaDebt); if (amount >= maxRepayment) { amount = maxRepayment; // U:[CM-11] } poolUnderlying.safeTransferFrom(creditor, address(pool), amount); if (amount == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( amount, // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] } if (deltaCollateral > 0) { uint256 amount = deltaCollateral.toUint256(); token.safeTransferFrom(collateralizer, address(this), amount); } else if (deltaCollateral < 0) { uint256 amount = abs(deltaCollateral); token.safeTransfer(collateralizer, amount); } position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt); VaultConfig memory config = vaultConfig; uint256 spotPrice_ = spotPrice(); uint256 collateralValue = wmul(position.collateral, spotPrice_);
if ( (deltaDebt > 0 || deltaCollateral < 0) && !_isCollateralized(newDebt, collateralValue, config.liquidationRatio) ) revert CDPVault__modifyCollateralAndDebt_notSafe(); emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt); }

[WP-H4] PoolV3.sol Interest Cannot Be Distributed to Equity Holders

No code for interest accounting was found.

Instead, we noticed that the share price math used in PoolV3 deposit and withdrawal operations is always math.

@@ 271,275 @@ /// @notice Withdraws pool shares for given amount of underlying tokens /// @param assets Amount of underlying to withdraw /// @param receiver Account to send underlying to /// @param owner Account to burn pool shares from /// @return shares Number of pool shares burned
function withdraw( uint256 assets, address receiver, address owner )
@@ 281,286 @@ public override(ERC4626, IERC4626) whenNotPaused // U:[LP-2A] whenNotLocked nonReentrant // U:[LP-2B] nonZeroAddress(receiver) // U:[LP-5]
returns (uint256 shares) { uint256 assetsToUser = _amountWithFee(assets); uint256 assetsSent = _amountWithWithdrawalFee(assetsToUser); // U:[LP-8] shares = _convertToShares(assetsSent); // U:[LP-8] _withdraw(receiver, owner, assetsSent, assets, assetsToUser, shares); // U:[LP-8] }
@@ 295,299 @@ /// @notice Redeems given number of pool shares for underlying tokens /// @param shares Number of pool shares to redeem /// @param receiver Account to send underlying to /// @param owner Account to burn pool shares from /// @return assets Amount of underlying withdrawn
function redeem( uint256 shares, address receiver, address owner )
@@ 305,310 @@ public override(ERC4626, IERC4626) whenNotPaused // U:[LP-2A] whenNotLocked nonReentrant // U:[LP-2B] nonZeroAddress(receiver) // U:[LP-5]
returns (uint256 assets) { uint256 assetsSent = _convertToAssets(shares); // U:[LP-9] uint256 assetsToUser = _amountMinusWithdrawalFee(assetsSent); assets = _amountMinusFee(assetsToUser); // U:[LP-9] _withdraw(receiver, owner, assetsSent, assets, assetsToUser, shares); // U:[LP-9] }
    /// @dev Internal conversion function (from assets to shares) with support for rounding direction
    /// @dev Pool is not vulnerable to the inflation attack, so the simplified implementation w/o virtual shares is used
    function _convertToShares(uint256 assets) internal pure returns (uint256 shares) {
        // uint256 supply = totalSupply();
        return assets; //(assets == 0 || supply == 0) ? assets : assets.mulDiv(supply, totalAssets(), rounding);
    }

    /// @dev Internal conversion function (from shares to assets) with support for rounding direction
    /// @dev Pool is not vulnerable to the inflation attack, so the simplified implementation w/o virtual shares is used
    function _convertToAssets(uint256 shares) internal pure returns (uint256 assets) {
        //uint256 supply = totalSupply();
        return shares; //(supply == 0) ? shares : shares.mulDiv(totalAssets(), supply, rounding);
    }
@@ 271,275 @@ /// @notice Withdraws pool shares for given amount of underlying tokens /// @param assets Amount of underlying to withdraw /// @param receiver Account to send underlying to /// @param owner Account to burn pool shares from /// @return shares Number of pool shares burned
function withdraw( uint256 assets, address receiver, address owner )
@@ 281,286 @@ public override(ERC4626, IERC4626) whenNotPaused // U:[LP-2A] whenNotLocked nonReentrant // U:[LP-2B] nonZeroAddress(receiver) // U:[LP-5]
returns (uint256 shares) { uint256 assetsToUser = _amountWithFee(assets); uint256 assetsSent = _amountWithWithdrawalFee(assetsToUser); // U:[LP-8] shares = _convertToShares(assetsSent); // U:[LP-8] _withdraw(receiver, owner, assetsSent, assets, assetsToUser, shares); // U:[LP-8] }
@@ 295,299 @@ /// @notice Redeems given number of pool shares for underlying tokens /// @param shares Number of pool shares to redeem /// @param receiver Account to send underlying to /// @param owner Account to burn pool shares from /// @return assets Amount of underlying withdrawn
function redeem( uint256 shares, address receiver, address owner )
@@ 305,310 @@ public override(ERC4626, IERC4626) whenNotPaused // U:[LP-2A] whenNotLocked nonReentrant // U:[LP-2B] nonZeroAddress(receiver) // U:[LP-5]
returns (uint256 assets) { uint256 assetsSent = _convertToAssets(shares); // U:[LP-9] uint256 assetsToUser = _amountMinusWithdrawalFee(assetsSent); assets = _amountMinusFee(assetsToUser); // U:[LP-9] _withdraw(receiver, owner, assetsSent, assets, assetsToUser, shares); // U:[LP-9] }
    /// @dev Internal conversion function (from assets to shares) with support for rounding direction
    /// @dev Pool is not vulnerable to the inflation attack, so the simplified implementation w/o virtual shares is used
    function _convertToShares(uint256 assets) internal pure returns (uint256 shares) {
        // uint256 supply = totalSupply();
        return assets; //(assets == 0 || supply == 0) ? assets : assets.mulDiv(supply, totalAssets(), rounding);
    }

    /// @dev Internal conversion function (from shares to assets) with support for rounding direction
    /// @dev Pool is not vulnerable to the inflation attack, so the simplified implementation w/o virtual shares is used
    function _convertToAssets(uint256 shares) internal pure returns (uint256 assets) {
        //uint256 supply = totalSupply();
        return shares; //(supply == 0) ? shares : shares.mulDiv(totalAssets(), supply, rounding);
    }

[WP-H5] CDPVault.liquidatePosition() should collect repayAmount from msg.sender, not deltaDebt

  • When takeCollateral <= position.collateral:

    penalty is not collected from msg.sender, CDPVault.sol#L549 lacks corresponding asset.

  • When takeCollateral > position.collateral:

    Excess asset is collected from msg.sender. Expected to collect repayAmount corresponding to takeCollateral CDPVault.sol#L507 from msg.sender.

    However, current implementation may collect all remaining positions[owner].debt, potentially far exceeding takeCollateral.

@@ 468,475 @@/// @notice Liquidates a single unsafe positions by selling collateral at a discounted (`liquidationDiscount`) /// oracle price. The liquidator has to provide the amount he wants to repay or sell (`repayAmounts`) for /// the position. From that repay amount a penalty (`liquidationPenalty`) is subtracted to mitigate against /// profitable self liquidations. If the available collateral of a position is not sufficient to cover the debt /// the vault accumulates 'bad debt'. /// @dev The liquidator has to approve the vault to transfer the sum of `repayAmounts`. /// @param owner Owner of the position to liquidate /// @param repayAmount Amount the liquidator wants to repay [wad]
function liquidatePosition(address owner, uint256 repayAmount) external whenNotPaused { // validate params if (owner == address(0) || repayAmount == 0) revert CDPVault__liquidatePosition_invalidParameters(); // load configs VaultConfig memory config = vaultConfig; LiquidationConfig memory liqConfig_ = liquidationConfig; // load liquidated position Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); // load price and calculate discounted price uint256 spotPrice_ = spotPrice(); uint256 discountedPrice = wmul(spotPrice_, liqConfig_.liquidationDiscount); if (spotPrice_ == 0) revert CDPVault__liquidatePosition_invalidSpotPrice(); // compute collateral to take, debt to repay and penalty to pay uint256 takeCollateral = wdiv(repayAmount, discountedPrice); uint256 deltaDebt = wmul(repayAmount, liqConfig_.liquidationPenalty); uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty);
@@ 497,499 @@
// verify that the position is indeed unsafe if (_isCollateralized(debtData.debt, wmul(position.collateral, spotPrice_), config.liquidationRatio)) revert CDPVault__liquidatePosition_notUnsafe(); // account for bad debt // TODO: review this if (takeCollateral > position.collateral) { takeCollateral = position.collateral; repayAmount = wmul(takeCollateral, discountedPrice); penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty); // debt >= repayAmount if takeCollateral > position.collateral //deltaDebt = currentDebt; deltaDebt = debtData.debt; } // update vault state totalDebt -= deltaDebt; // transfer the repay amount from the liquidator to the vault poolUnderlying.safeTransferFrom(msg.sender, address(pool), deltaDebt);
@@ 521,542 @@ uint256 newDebt; uint256 profit; uint256 maxRepayment = calcTotalDebt(debtData); { uint256 newCumulativeIndex; if (deltaDebt == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( deltaDebt, // delta debt debtData.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray debtData.cumulativeIndexLastUpdate ); } // update liquidated position position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, -toInt256(takeCollateral), totalDebt); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11]
// transfer the collateral amount from the vault to the liquidator // cash[msg.sender] += takeCollateral; token.safeTransfer(msg.sender, takeCollateral); // Mint the penalty from the vault to the treasury // cdm.modifyBalance(address(this), address(buffer), penalty); IPoolV3Loop(address(pool)).mintProfit(penalty);
@@ 550,551 @@
}
@@ 468,475 @@/// @notice Liquidates a single unsafe positions by selling collateral at a discounted (`liquidationDiscount`) /// oracle price. The liquidator has to provide the amount he wants to repay or sell (`repayAmounts`) for /// the position. From that repay amount a penalty (`liquidationPenalty`) is subtracted to mitigate against /// profitable self liquidations. If the available collateral of a position is not sufficient to cover the debt /// the vault accumulates 'bad debt'. /// @dev The liquidator has to approve the vault to transfer the sum of `repayAmounts`. /// @param owner Owner of the position to liquidate /// @param repayAmount Amount the liquidator wants to repay [wad]
function liquidatePosition(address owner, uint256 repayAmount) external whenNotPaused { // validate params if (owner == address(0) || repayAmount == 0) revert CDPVault__liquidatePosition_invalidParameters(); // load configs VaultConfig memory config = vaultConfig; LiquidationConfig memory liqConfig_ = liquidationConfig; // load liquidated position Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); // load price and calculate discounted price uint256 spotPrice_ = spotPrice(); uint256 discountedPrice = wmul(spotPrice_, liqConfig_.liquidationDiscount); if (spotPrice_ == 0) revert CDPVault__liquidatePosition_invalidSpotPrice(); // compute collateral to take, debt to repay and penalty to pay uint256 takeCollateral = wdiv(repayAmount, discountedPrice); uint256 deltaDebt = wmul(repayAmount, liqConfig_.liquidationPenalty); uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty);
@@ 497,499 @@
// verify that the position is indeed unsafe if (_isCollateralized(debtData.debt, wmul(position.collateral, spotPrice_), config.liquidationRatio)) revert CDPVault__liquidatePosition_notUnsafe(); // account for bad debt // TODO: review this if (takeCollateral > position.collateral) { takeCollateral = position.collateral; repayAmount = wmul(takeCollateral, discountedPrice); penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty); // debt >= repayAmount if takeCollateral > position.collateral //deltaDebt = currentDebt; deltaDebt = debtData.debt; } // update vault state totalDebt -= deltaDebt; // transfer the repay amount from the liquidator to the vault poolUnderlying.safeTransferFrom(msg.sender, address(pool), deltaDebt);
@@ 521,542 @@ uint256 newDebt; uint256 profit; uint256 maxRepayment = calcTotalDebt(debtData); { uint256 newCumulativeIndex; if (deltaDebt == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( deltaDebt, // delta debt debtData.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray debtData.cumulativeIndexLastUpdate ); } // update liquidated position position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, -toInt256(takeCollateral), totalDebt); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11]
// transfer the collateral amount from the vault to the liquidator // cash[msg.sender] += takeCollateral; token.safeTransfer(msg.sender, takeCollateral); // Mint the penalty from the vault to the treasury // cdm.modifyBalance(address(this), address(buffer), penalty); IPoolV3Loop(address(pool)).mintProfit(penalty);
@@ 550,551 @@
}

[WP-H6] Failure to update lastBaseInterestUpdate results in recalculation of _baseInterestIndexLU on every call, leading to excessive interest accrual

Due to lastBaseInterestUpdate being updated only during deployment, the calculation of time elapsed since the last update in CreditLogic.sol#L27 results in a large value. This causes rapid growth of the calculated interest index, accruing excessive interest on user positions.

function calcLinearGrowth(uint256 value, uint256 timestampLastUpdate) internal view returns (uint256) {
    return value * (block.timestamp - timestampLastUpdate) / SECONDS_PER_YEAR;
}
function _updateBaseInterest(
    int256 expectedLiquidityDelta,
    int256 availableLiquidityDelta,
    bool checkOptimalBorrowing
) internal {
    uint256 expectedLiquidity_ = (expectedLiquidity().toInt256() + expectedLiquidityDelta).toUint256();
    uint256 availableLiquidity_ = (availableLiquidity().toInt256() + availableLiquidityDelta).toUint256();

    uint256 lastBaseInterestUpdate_ = lastBaseInterestUpdate;
    if (block.timestamp != lastBaseInterestUpdate_) {
        _baseInterestIndexLU = _calcBaseInterestIndex(lastBaseInterestUpdate_).toUint128(); // U:[LP-18]
    }

    if (block.timestamp != lastQuotaRevenueUpdate) {
        lastQuotaRevenueUpdate = uint40(block.timestamp); // U:[LP-18]
    }

    _expectedLiquidityLU = expectedLiquidity_.toUint128(); // U:[LP-18]
    _baseInterestRate = ILinearInterestRateModelV3(interestRateModel)
        .calcBorrowRate({
            expectedLiquidity: expectedLiquidity_,
            availableLiquidity: availableLiquidity_,
            checkOptimalBorrowing: checkOptimalBorrowing
        })
        .toUint128(); // U:[LP-18]
}
function _calcBaseInterestIndex(uint256 timestamp) private view returns (uint256) {
    return (_baseInterestIndexLU * (RAY + baseInterestRate().calcLinearGrowth(timestamp))) / RAY;
}
function baseInterestRate() public view override returns (uint256) {
    return _baseInterestRate;
}
constructor(
@@ 140,161 @@ address addressProvider_, address underlyingToken_, address interestRateModel_, uint256 totalDebtLimit_, string memory name_, string memory symbol_ ) ACLNonReentrantTrait(addressProvider_) // U:[LP-1A] ContractsRegisterTrait(addressProvider_) ERC4626(IERC20(underlyingToken_)) // U:[LP-1B] ERC20(name_, symbol_) // U:[LP-1B] ERC20Permit(name_) // U:[LP-1B] nonZeroAddress(underlyingToken_) // U:[LP-1A] nonZeroAddress(interestRateModel_) // U:[LP-1A] { addressProvider = addressProvider_; // U:[LP-1B] underlyingToken = underlyingToken_; // U:[LP-1B] treasury = IAddressProviderV3(addressProvider_).getAddressOrRevert({ key: AP_TREASURY, _version: NO_VERSION_CONTROL }); // U:[LP-1B]
lastBaseInterestUpdate = uint40(block.timestamp); // U:[LP-1B] _baseInterestIndexLU = uint128(RAY); // U:[LP-1B]
@@ 166,172 @@ interestRateModel = interestRateModel_; // U:[LP-1B] emit SetInterestRateModel(interestRateModel_); // U:[LP-1B] locked = true; _setTotalDebtLimit(totalDebtLimit_); // U:[LP-1B] _creditManagerSet.add(msg.sender);
}

Recommendation

Update lastBaseInterestUpdate at PoolV3.sol#L631:

PoolV3.sol

function _updateBaseInterest(
    int256 expectedLiquidityDelta,
    int256 availableLiquidityDelta,
    bool checkOptimalBorrowing
) internal {
    uint256 expectedLiquidity_ = (expectedLiquidity().toInt256() + expectedLiquidityDelta).toUint256();
    uint256 availableLiquidity_ = (availableLiquidity().toInt256() + availableLiquidityDelta).toUint256();

    uint256 lastBaseInterestUpdate_ = lastBaseInterestUpdate;
    if (block.timestamp != lastBaseInterestUpdate_) {
        _baseInterestIndexLU = _calcBaseInterestIndex(lastBaseInterestUpdate_).toUint128(); // U:[LP-18]
        lastBaseInterestUpdate = block.timestamp;
    }

    if (block.timestamp != lastQuotaRevenueUpdate) {
        lastQuotaRevenueUpdate = uint40(block.timestamp); // U:[LP-18]
    }

    _expectedLiquidityLU = expectedLiquidity_.toUint128(); // U:[LP-18]
    _baseInterestRate = ILinearInterestRateModelV3(interestRateModel)
        .calcBorrowRate({
            expectedLiquidity: expectedLiquidity_,
            availableLiquidity: availableLiquidity_,
            checkOptimalBorrowing: checkOptimalBorrowing
        })
        .toUint128(); // U:[LP-18]
}
function calcLinearGrowth(uint256 value, uint256 timestampLastUpdate) internal view returns (uint256) {
    return value * (block.timestamp - timestampLastUpdate) / SECONDS_PER_YEAR;
}
function _updateBaseInterest(
    int256 expectedLiquidityDelta,
    int256 availableLiquidityDelta,
    bool checkOptimalBorrowing
) internal {
    uint256 expectedLiquidity_ = (expectedLiquidity().toInt256() + expectedLiquidityDelta).toUint256();
    uint256 availableLiquidity_ = (availableLiquidity().toInt256() + availableLiquidityDelta).toUint256();

    uint256 lastBaseInterestUpdate_ = lastBaseInterestUpdate;
    if (block.timestamp != lastBaseInterestUpdate_) {
        _baseInterestIndexLU = _calcBaseInterestIndex(lastBaseInterestUpdate_).toUint128(); // U:[LP-18]
    }

    if (block.timestamp != lastQuotaRevenueUpdate) {
        lastQuotaRevenueUpdate = uint40(block.timestamp); // U:[LP-18]
    }

    _expectedLiquidityLU = expectedLiquidity_.toUint128(); // U:[LP-18]
    _baseInterestRate = ILinearInterestRateModelV3(interestRateModel)
        .calcBorrowRate({
            expectedLiquidity: expectedLiquidity_,
            availableLiquidity: availableLiquidity_,
            checkOptimalBorrowing: checkOptimalBorrowing
        })
        .toUint128(); // U:[LP-18]
}
function _calcBaseInterestIndex(uint256 timestamp) private view returns (uint256) {
    return (_baseInterestIndexLU * (RAY + baseInterestRate().calcLinearGrowth(timestamp))) / RAY;
}
function baseInterestRate() public view override returns (uint256) {
    return _baseInterestRate;
}
constructor(
@@ 140,161 @@ address addressProvider_, address underlyingToken_, address interestRateModel_, uint256 totalDebtLimit_, string memory name_, string memory symbol_ ) ACLNonReentrantTrait(addressProvider_) // U:[LP-1A] ContractsRegisterTrait(addressProvider_) ERC4626(IERC20(underlyingToken_)) // U:[LP-1B] ERC20(name_, symbol_) // U:[LP-1B] ERC20Permit(name_) // U:[LP-1B] nonZeroAddress(underlyingToken_) // U:[LP-1A] nonZeroAddress(interestRateModel_) // U:[LP-1A] { addressProvider = addressProvider_; // U:[LP-1B] underlyingToken = underlyingToken_; // U:[LP-1B] treasury = IAddressProviderV3(addressProvider_).getAddressOrRevert({ key: AP_TREASURY, _version: NO_VERSION_CONTROL }); // U:[LP-1B]
lastBaseInterestUpdate = uint40(block.timestamp); // U:[LP-1B] _baseInterestIndexLU = uint128(RAY); // U:[LP-1B]
@@ 166,172 @@ interestRateModel = interestRateModel_; // U:[LP-1B] emit SetInterestRateModel(interestRateModel_); // U:[LP-1B] locked = true; _setTotalDebtLimit(totalDebtLimit_); // U:[LP-1B] _creditManagerSet.add(msg.sender);
}

[WP-H7] In the case of bad debt, deltaDebt is not actually repaid, making the profit calculation based on this figure inaccurate.

When takeCollateral exceeds position.collateral during liquidation, deltaDebt represents the full debt of the position. However, repayAmount is likely to be less than deltaDebt in this scenario.

Nevertheless, calcDecrease still uses deltaDebt to calculate profit. In reality, the system may not have recovered even the principal of the position at this point, meaning there might be no profit and potentially a loss instead.

@@ 468,475 @@/// @notice Liquidates a single unsafe positions by selling collateral at a discounted (`liquidationDiscount`) /// oracle price. The liquidator has to provide the amount he wants to repay or sell (`repayAmounts`) for /// the position. From that repay amount a penalty (`liquidationPenalty`) is subtracted to mitigate against /// profitable self liquidations. If the available collateral of a position is not sufficient to cover the debt /// the vault accumulates 'bad debt'. /// @dev The liquidator has to approve the vault to transfer the sum of `repayAmounts`. /// @param owner Owner of the position to liquidate /// @param repayAmount Amount the liquidator wants to repay [wad]
function liquidatePosition(address owner, uint256 repayAmount) external whenNotPaused {
@@ 477,503 @@ // validate params if (owner == address(0) || repayAmount == 0) revert CDPVault__liquidatePosition_invalidParameters(); // load configs VaultConfig memory config = vaultConfig; LiquidationConfig memory liqConfig_ = liquidationConfig; // load liquidated position Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); // load price and calculate discounted price uint256 spotPrice_ = spotPrice(); uint256 discountedPrice = wmul(spotPrice_, liqConfig_.liquidationDiscount); if (spotPrice_ == 0) revert CDPVault__liquidatePosition_invalidSpotPrice(); // compute collateral to take, debt to repay and penalty to pay uint256 takeCollateral = wdiv(repayAmount, discountedPrice); uint256 deltaDebt = wmul(repayAmount, liqConfig_.liquidationPenalty); uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty); // verify that the position is indeed unsafe if (_isCollateralized(debtData.debt, wmul(position.collateral, spotPrice_), config.liquidationRatio)) revert CDPVault__liquidatePosition_notUnsafe();
// account for bad debt // TODO: review this if (takeCollateral > position.collateral) { takeCollateral = position.collateral; repayAmount = wmul(takeCollateral, discountedPrice); penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty); // debt >= repayAmount if takeCollateral > position.collateral //deltaDebt = currentDebt; deltaDebt = debtData.debt; } // update vault state totalDebt -= deltaDebt; // transfer the repay amount from the liquidator to the vault poolUnderlying.safeTransferFrom(msg.sender, address(pool), deltaDebt); uint256 newDebt; uint256 profit; uint256 maxRepayment = calcTotalDebt(debtData); { uint256 newCumulativeIndex; if (deltaDebt == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( deltaDebt, // delta debt debtData.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray debtData.cumulativeIndexLastUpdate ); } // update liquidated position position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, -toInt256(takeCollateral), totalDebt); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] // transfer the collateral amount from the vault to the liquidator // cash[msg.sender] += takeCollateral; token.safeTransfer(msg.sender, takeCollateral); // Mint the penalty from the vault to the treasury // cdm.modifyBalance(address(this), address(buffer), penalty); IPoolV3Loop(address(pool)).mintProfit(penalty); }
function calcDecrease(
        uint256 amount,
        uint256 debt,
        uint256 cumulativeIndexNow,
        uint256 cumulativeIndexLastUpdate
    ) internal view returns (uint256 newDebt, uint256 newCumulativeIndex, uint256 profit) {
        uint256 amountToRepay = amount;

        if (amountToRepay != 0) {
            uint256 interestAccrued = calcAccruedInterest({
                amount: debt,
                cumulativeIndexLastUpdate: cumulativeIndexLastUpdate,
                cumulativeIndexNow: cumulativeIndexNow
            }); // U:[CL-3]
            uint256 profitFromInterest = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; // U:[CL-3]

            if (amountToRepay >= interestAccrued + profitFromInterest) {
                amountToRepay -= interestAccrued + profitFromInterest;

                profit += profitFromInterest; // U:[CL-3]

                newCumulativeIndex = cumulativeIndexNow; // U:[CL-3]
            } else {
                // If amount is not enough to repay base interest + DAO fee, then it is split pro-rata between them
                uint256 amountToPool = (amountToRepay * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR + feeInterest);

                profit += amountToRepay - amountToPool; // U:[CL-3]
                amountToRepay = 0; // U:[CL-3]

                newCumulativeIndex =
                    (INDEX_PRECISION * cumulativeIndexNow * cumulativeIndexLastUpdate) /
                    (INDEX_PRECISION *
                        cumulativeIndexNow -
                        (INDEX_PRECISION * amountToPool * cumulativeIndexLastUpdate) /
                        debt); // U:[CL-3]
            }
        } else {
            newCumulativeIndex = cumulativeIndexLastUpdate; // U:[CL-3]
        }
        newDebt = debt - amountToRepay; // U:[CL-3]
    }
function repayCreditAccount(
        uint256 repaidAmount,
        uint256 profit,
        uint256 loss
    )
        external
        override
        whenNotPaused // U:[LP-2A]
        nonReentrant // U:[LP-2B]
    {
        uint128 repaidAmountU128 = repaidAmount.toUint128();

        DebtParams storage cmDebt = _creditManagerDebt[msg.sender];
        uint128 cmBorrowed = cmDebt.borrowed;
        if (cmBorrowed == 0) {
            revert CallerNotCreditManagerException(); // U:[LP-2C,14A]
        }

        if (profit > 0) {
            _mint(treasury, convertToShares(profit)); // U:[LP-14B]
        } else if (loss > 0) {
            address treasury_ = treasury;
            uint256 sharesInTreasury = balanceOf(treasury_);
            uint256 sharesToBurn = convertToShares(loss);
            if (sharesToBurn > sharesInTreasury) {
                unchecked {
                    emit IncurUncoveredLoss({
                        creditManager: msg.sender,
                        loss: convertToAssets(sharesToBurn - sharesInTreasury)
                    }); // U:[LP-14D]
                }
                sharesToBurn = sharesInTreasury;
            }
            _burn(treasury_, sharesToBurn); // U:[LP-14C,14D]
        }

        _updateBaseInterest({
            expectedLiquidityDelta: profit.toInt256() - loss.toInt256(),
            availableLiquidityDelta: 0,
            checkOptimalBorrowing: false
        }); // U:[LP-14B,14C,14D]

        _totalDebt.borrowed -= repaidAmountU128; // U:[LP-14B,14C,14D]
        cmDebt.borrowed = cmBorrowed - repaidAmountU128; // U:[LP-14B,14C,14D]

        emit Repay(msg.sender, repaidAmount, profit, loss); // U:[LP-14B,14C,14D]
    }

Recommendation

The calcDecrease function is only applicable for partial repayments under normal circumstances. In cases of bad debt, a separate function is required to calculate the loss.

@@ 468,475 @@/// @notice Liquidates a single unsafe positions by selling collateral at a discounted (`liquidationDiscount`) /// oracle price. The liquidator has to provide the amount he wants to repay or sell (`repayAmounts`) for /// the position. From that repay amount a penalty (`liquidationPenalty`) is subtracted to mitigate against /// profitable self liquidations. If the available collateral of a position is not sufficient to cover the debt /// the vault accumulates 'bad debt'. /// @dev The liquidator has to approve the vault to transfer the sum of `repayAmounts`. /// @param owner Owner of the position to liquidate /// @param repayAmount Amount the liquidator wants to repay [wad]
function liquidatePosition(address owner, uint256 repayAmount) external whenNotPaused {
@@ 477,503 @@ // validate params if (owner == address(0) || repayAmount == 0) revert CDPVault__liquidatePosition_invalidParameters(); // load configs VaultConfig memory config = vaultConfig; LiquidationConfig memory liqConfig_ = liquidationConfig; // load liquidated position Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); // load price and calculate discounted price uint256 spotPrice_ = spotPrice(); uint256 discountedPrice = wmul(spotPrice_, liqConfig_.liquidationDiscount); if (spotPrice_ == 0) revert CDPVault__liquidatePosition_invalidSpotPrice(); // compute collateral to take, debt to repay and penalty to pay uint256 takeCollateral = wdiv(repayAmount, discountedPrice); uint256 deltaDebt = wmul(repayAmount, liqConfig_.liquidationPenalty); uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty); // verify that the position is indeed unsafe if (_isCollateralized(debtData.debt, wmul(position.collateral, spotPrice_), config.liquidationRatio)) revert CDPVault__liquidatePosition_notUnsafe();
// account for bad debt // TODO: review this if (takeCollateral > position.collateral) { takeCollateral = position.collateral; repayAmount = wmul(takeCollateral, discountedPrice); penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty); // debt >= repayAmount if takeCollateral > position.collateral //deltaDebt = currentDebt; deltaDebt = debtData.debt; } // update vault state totalDebt -= deltaDebt; // transfer the repay amount from the liquidator to the vault poolUnderlying.safeTransferFrom(msg.sender, address(pool), deltaDebt); uint256 newDebt; uint256 profit; uint256 maxRepayment = calcTotalDebt(debtData); { uint256 newCumulativeIndex; if (deltaDebt == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( deltaDebt, // delta debt debtData.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray debtData.cumulativeIndexLastUpdate ); } // update liquidated position position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, -toInt256(takeCollateral), totalDebt); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] // transfer the collateral amount from the vault to the liquidator // cash[msg.sender] += takeCollateral; token.safeTransfer(msg.sender, takeCollateral); // Mint the penalty from the vault to the treasury // cdm.modifyBalance(address(this), address(buffer), penalty); IPoolV3Loop(address(pool)).mintProfit(penalty); }
function calcDecrease(
        uint256 amount,
        uint256 debt,
        uint256 cumulativeIndexNow,
        uint256 cumulativeIndexLastUpdate
    ) internal view returns (uint256 newDebt, uint256 newCumulativeIndex, uint256 profit) {
        uint256 amountToRepay = amount;

        if (amountToRepay != 0) {
            uint256 interestAccrued = calcAccruedInterest({
                amount: debt,
                cumulativeIndexLastUpdate: cumulativeIndexLastUpdate,
                cumulativeIndexNow: cumulativeIndexNow
            }); // U:[CL-3]
            uint256 profitFromInterest = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; // U:[CL-3]

            if (amountToRepay >= interestAccrued + profitFromInterest) {
                amountToRepay -= interestAccrued + profitFromInterest;

                profit += profitFromInterest; // U:[CL-3]

                newCumulativeIndex = cumulativeIndexNow; // U:[CL-3]
            } else {
                // If amount is not enough to repay base interest + DAO fee, then it is split pro-rata between them
                uint256 amountToPool = (amountToRepay * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR + feeInterest);

                profit += amountToRepay - amountToPool; // U:[CL-3]
                amountToRepay = 0; // U:[CL-3]

                newCumulativeIndex =
                    (INDEX_PRECISION * cumulativeIndexNow * cumulativeIndexLastUpdate) /
                    (INDEX_PRECISION *
                        cumulativeIndexNow -
                        (INDEX_PRECISION * amountToPool * cumulativeIndexLastUpdate) /
                        debt); // U:[CL-3]
            }
        } else {
            newCumulativeIndex = cumulativeIndexLastUpdate; // U:[CL-3]
        }
        newDebt = debt - amountToRepay; // U:[CL-3]
    }
function repayCreditAccount(
        uint256 repaidAmount,
        uint256 profit,
        uint256 loss
    )
        external
        override
        whenNotPaused // U:[LP-2A]
        nonReentrant // U:[LP-2B]
    {
        uint128 repaidAmountU128 = repaidAmount.toUint128();

        DebtParams storage cmDebt = _creditManagerDebt[msg.sender];
        uint128 cmBorrowed = cmDebt.borrowed;
        if (cmBorrowed == 0) {
            revert CallerNotCreditManagerException(); // U:[LP-2C,14A]
        }

        if (profit > 0) {
            _mint(treasury, convertToShares(profit)); // U:[LP-14B]
        } else if (loss > 0) {
            address treasury_ = treasury;
            uint256 sharesInTreasury = balanceOf(treasury_);
            uint256 sharesToBurn = convertToShares(loss);
            if (sharesToBurn > sharesInTreasury) {
                unchecked {
                    emit IncurUncoveredLoss({
                        creditManager: msg.sender,
                        loss: convertToAssets(sharesToBurn - sharesInTreasury)
                    }); // U:[LP-14D]
                }
                sharesToBurn = sharesInTreasury;
            }
            _burn(treasury_, sharesToBurn); // U:[LP-14C,14D]
        }

        _updateBaseInterest({
            expectedLiquidityDelta: profit.toInt256() - loss.toInt256(),
            availableLiquidityDelta: 0,
            checkOptimalBorrowing: false
        }); // U:[LP-14B,14C,14D]

        _totalDebt.borrowed -= repaidAmountU128; // U:[LP-14B,14C,14D]
        cmDebt.borrowed = cmBorrowed - repaidAmountU128; // U:[LP-14B,14C,14D]

        emit Repay(msg.sender, repaidAmount, profit, loss); // U:[LP-14B,14C,14D]
    }

[WP-M8] PositionAction20.deposit() and PositionAction4626.deposit() will revert due to unexpected double execution of modifyCollateralAndDebt()

The second deposit call will revert due to insufficient allowance.

@@ 180,183 @@/// @notice Adds collateral to a CDP Vault /// @param position The CDP Vault position /// @param vault The CDP Vault /// @param collateralParams The collateral parameters
function deposit( address position, address vault, CollateralParams calldata collateralParams, PermitParams calldata permitParams ) external onlyDelegatecall { uint256 collateral = _deposit(vault, collateralParams, permitParams); ICDPVault(vault).modifyCollateralAndDebt(position, address(this), address(this), toInt256(collateral), 0); }
/// @notice Deposits collateral into CDPVault (optionally transfer and swaps an arbitrary token to collateral)
/// @param vault The CDP Vault
/// @param collateralParams The collateral parameters
/// @return The amount of collateral deposited [wad]
function _deposit(
    address vault,
    CollateralParams calldata collateralParams,
    PermitParams calldata permitParams
) internal returns (uint256) {
    uint256 amount = collateralParams.amount;

@@ 537,551 @@ if (collateralParams.auxSwap.assetIn != address(0)) { if ( collateralParams.auxSwap.assetIn != collateralParams.targetToken || collateralParams.auxSwap.recipient != address(this) ) revert PositionAction__deposit_InvalidAuxSwap(); amount = _transferAndSwap(collateralParams.collateralizer, collateralParams.auxSwap, permitParams); } else if (collateralParams.collateralizer != address(this)) { _transferFrom( collateralParams.targetToken, collateralParams.collateralizer, address(this), amount, permitParams ); }
return _onDeposit(vault, collateralParams.targetToken, amount); }
@@ 34,37 @@/// @notice Deposit collateral into the vault /// @param vault Address of the vault /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()] /// @return Amount of collateral deposited [wad]
function _onDeposit(address vault, address /*src*/, uint256 amount) internal override returns (uint256) { address collateralToken = address(ICDPVault(vault).token()); IERC20(collateralToken).forceApprove(vault, amount); return ICDPVault(vault).deposit(address(this), amount); }
@@ 35,39 @@/// @notice Deposit collateral into the vault /// @param vault Address of the vault /// @param src Token passed in by the caller /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()] /// @return Amount of collateral deposited [wad]
function _onDeposit(address vault, address src, uint256 amount) internal override returns (uint256) { address collateral = address(ICDPVault(vault).token());
@@ 43,48 @@ // if the src is not the collateralToken, we need to deposit the underlying into the ERC4626 vault if (src != collateral) { address underlying = IERC4626(collateral).asset(); IERC20(underlying).forceApprove(collateral, amount); amount = IERC4626(collateral).deposit(amount, address(this)); }
IERC20(collateral).forceApprove(vault, amount); return ICDPVault(vault).deposit(address(this), amount); }
@@ 224,228 @@/// @notice Deposits collateral tokens into this contract and increases a users collateral balance /// @dev The caller needs to approve this contract to transfer tokens on their behalf /// @param to Address of the user to attribute the collateral to /// @param amount Amount of tokens to deposit [tokenScale] /// @return tokenAmount Amount of collateral deposited [wad]
function deposit(address to, uint256 amount) external whenNotPaused returns (uint256 tokenAmount) { tokenAmount = wdiv(amount, tokenScale); int256 deltaCollateral = toInt256(tokenAmount); modifyCollateralAndDebt({ owner: to, collateralizer: msg.sender, creditor: msg.sender, deltaCollateral: deltaCollateral, deltaDebt: 0 }); }
@@ 180,183 @@/// @notice Adds collateral to a CDP Vault /// @param position The CDP Vault position /// @param vault The CDP Vault /// @param collateralParams The collateral parameters
function deposit( address position, address vault, CollateralParams calldata collateralParams, PermitParams calldata permitParams ) external onlyDelegatecall { uint256 collateral = _deposit(vault, collateralParams, permitParams); ICDPVault(vault).modifyCollateralAndDebt(position, address(this), address(this), toInt256(collateral), 0); }
/// @notice Deposits collateral into CDPVault (optionally transfer and swaps an arbitrary token to collateral)
/// @param vault The CDP Vault
/// @param collateralParams The collateral parameters
/// @return The amount of collateral deposited [wad]
function _deposit(
    address vault,
    CollateralParams calldata collateralParams,
    PermitParams calldata permitParams
) internal returns (uint256) {
    uint256 amount = collateralParams.amount;

@@ 537,551 @@ if (collateralParams.auxSwap.assetIn != address(0)) { if ( collateralParams.auxSwap.assetIn != collateralParams.targetToken || collateralParams.auxSwap.recipient != address(this) ) revert PositionAction__deposit_InvalidAuxSwap(); amount = _transferAndSwap(collateralParams.collateralizer, collateralParams.auxSwap, permitParams); } else if (collateralParams.collateralizer != address(this)) { _transferFrom( collateralParams.targetToken, collateralParams.collateralizer, address(this), amount, permitParams ); }
return _onDeposit(vault, collateralParams.targetToken, amount); }
@@ 34,37 @@/// @notice Deposit collateral into the vault /// @param vault Address of the vault /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()] /// @return Amount of collateral deposited [wad]
function _onDeposit(address vault, address /*src*/, uint256 amount) internal override returns (uint256) { address collateralToken = address(ICDPVault(vault).token()); IERC20(collateralToken).forceApprove(vault, amount); return ICDPVault(vault).deposit(address(this), amount); }
@@ 35,39 @@/// @notice Deposit collateral into the vault /// @param vault Address of the vault /// @param src Token passed in by the caller /// @param amount Amount of collateral to deposit [CDPVault.tokenScale()] /// @return Amount of collateral deposited [wad]
function _onDeposit(address vault, address src, uint256 amount) internal override returns (uint256) { address collateral = address(ICDPVault(vault).token());
@@ 43,48 @@ // if the src is not the collateralToken, we need to deposit the underlying into the ERC4626 vault if (src != collateral) { address underlying = IERC4626(collateral).asset(); IERC20(underlying).forceApprove(collateral, amount); amount = IERC4626(collateral).deposit(amount, address(this)); }
IERC20(collateral).forceApprove(vault, amount); return ICDPVault(vault).deposit(address(this), amount); }
@@ 224,228 @@/// @notice Deposits collateral tokens into this contract and increases a users collateral balance /// @dev The caller needs to approve this contract to transfer tokens on their behalf /// @param to Address of the user to attribute the collateral to /// @param amount Amount of tokens to deposit [tokenScale] /// @return tokenAmount Amount of collateral deposited [wad]
function deposit(address to, uint256 amount) external whenNotPaused returns (uint256 tokenAmount) { tokenAmount = wdiv(amount, tokenScale); int256 deltaCollateral = toInt256(tokenAmount); modifyCollateralAndDebt({ owner: to, collateralizer: msg.sender, creditor: msg.sender, deltaCollateral: deltaCollateral, deltaDebt: 0 }); }

[WP-M9]CDPVault.deposit() incorrectly passes deltaCollateral to modifyCollateralAndDebt({ deltaCollateral: deltaCollateral, ... })

The deltaCollateral value in modifyCollateralAndDebt() should be in [tokenScale] units, as this amount will be used for directly transferring tokens. See also [WP-N17].

Current implementation: CDPVault.sol#L230-231 calculates deltaCollateral in WAD units.

    /// @notice Collateral token's decimals scale (10 ** decimals)
    uint256 public immutable tokenScale;
    /// @notice Deposits collateral tokens into this contract and increases a users collateral balance
    /// @dev The caller needs to approve this contract to transfer tokens on their behalf
    /// @param to Address of the user to attribute the collateral to
    /// @param amount Amount of tokens to deposit [tokenScale]
    /// @return tokenAmount Amount of collateral deposited [wad]
    function deposit(address to, uint256 amount) external whenNotPaused returns (uint256 tokenAmount) {
        tokenAmount = wdiv(amount, tokenScale);
        int256 deltaCollateral = toInt256(tokenAmount);
        modifyCollateralAndDebt({
            owner: to,
            collateralizer: msg.sender,
            creditor: msg.sender,
            deltaCollateral: deltaCollateral,
            deltaDebt: 0
        });
    }
/// @dev Equivalent to `(x * WAD) / y` rounded down.
/// @dev Taken from https://github.com/Vectorized/solady/blob/6d706e05ef43cbed234c648f83c55f3a4bb0a520/src/utils/FixedPointMathLib.sol#L84
function wdiv(uint256 x, uint256 y) pure returns (uint256 z) {
@@ 122,132 @@ assembly ("memory-safe") { // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`. if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) { // Store the function selector of `Math__div_overflow()`. mstore(0x00, 0xbcbede65) // Revert with (offset, size). revert(0x1c, 0x04) } z := div(mul(x, WAD), y) }
}
    /// @notice Collateral token's decimals scale (10 ** decimals)
    uint256 public immutable tokenScale;
    /// @notice Deposits collateral tokens into this contract and increases a users collateral balance
    /// @dev The caller needs to approve this contract to transfer tokens on their behalf
    /// @param to Address of the user to attribute the collateral to
    /// @param amount Amount of tokens to deposit [tokenScale]
    /// @return tokenAmount Amount of collateral deposited [wad]
    function deposit(address to, uint256 amount) external whenNotPaused returns (uint256 tokenAmount) {
        tokenAmount = wdiv(amount, tokenScale);
        int256 deltaCollateral = toInt256(tokenAmount);
        modifyCollateralAndDebt({
            owner: to,
            collateralizer: msg.sender,
            creditor: msg.sender,
            deltaCollateral: deltaCollateral,
            deltaDebt: 0
        });
    }
/// @dev Equivalent to `(x * WAD) / y` rounded down.
/// @dev Taken from https://github.com/Vectorized/solady/blob/6d706e05ef43cbed234c648f83c55f3a4bb0a520/src/utils/FixedPointMathLib.sol#L84
function wdiv(uint256 x, uint256 y) pure returns (uint256 z) {
@@ 122,132 @@ assembly ("memory-safe") { // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`. if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) { // Store the function selector of `Math__div_overflow()`. mstore(0x00, 0xbcbede65) // Revert with (offset, size). revert(0x1c, 0x04) } z := div(mul(x, WAD), y) }
}

[WP-M10] No code utilizing the loss parameter in repayCreditAccount.repayCreditAccount(..., uint256 loss) was found.

In CDPVault.liquidatePosition(), when the position is insolvent, pool.repayCreditAccount() should have a profit of 0 and a non-zero loss value.

https://github.com/LoopFi/loop-contracts/blob/44a7c95f7c83027078f58d2302c96b0716e0f053/src/CDPVault.sol#L468-L552

@@ 468,475 @@ /// @notice Liquidates a single unsafe positions by selling collateral at a discounted (`liquidationDiscount`) /// oracle price. The liquidator has to provide the amount he wants to repay or sell (`repayAmounts`) for /// the position. From that repay amount a penalty (`liquidationPenalty`) is subtracted to mitigate against /// profitable self liquidations. If the available collateral of a position is not sufficient to cover the debt /// the vault accumulates 'bad debt'. /// @dev The liquidator has to approve the vault to transfer the sum of `repayAmounts`. /// @param owner Owner of the position to liquidate /// @param repayAmount Amount the liquidator wants to repay [wad]
function liquidatePosition(address owner, uint256 repayAmount) external whenNotPaused { // validate params if (owner == address(0) || repayAmount == 0) revert CDPVault__liquidatePosition_invalidParameters(); // load configs VaultConfig memory config = vaultConfig; LiquidationConfig memory liqConfig_ = liquidationConfig; // load liquidated position Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); // load price and calculate discounted price uint256 spotPrice_ = spotPrice(); uint256 discountedPrice = wmul(spotPrice_, liqConfig_.liquidationDiscount); if (spotPrice_ == 0) revert CDPVault__liquidatePosition_invalidSpotPrice(); // compute collateral to take, debt to repay and penalty to pay uint256 takeCollateral = wdiv(repayAmount, discountedPrice); uint256 deltaDebt = wmul(repayAmount, liqConfig_.liquidationPenalty); uint256 penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty); // verify that the position is indeed unsafe if (_isCollateralized(debtData.debt, wmul(position.collateral, spotPrice_), config.liquidationRatio)) revert CDPVault__liquidatePosition_notUnsafe(); // account for bad debt // TODO: review this if (takeCollateral > position.collateral) { takeCollateral = position.collateral; repayAmount = wmul(takeCollateral, discountedPrice); penalty = wmul(repayAmount, WAD - liqConfig_.liquidationPenalty); // debt >= repayAmount if takeCollateral > position.collateral //deltaDebt = currentDebt; deltaDebt = debtData.debt; } // update vault state totalDebt -= deltaDebt; // transfer the repay amount from the liquidator to the vault poolUnderlying.safeTransferFrom(msg.sender, address(pool), deltaDebt); uint256 newDebt; uint256 profit; uint256 maxRepayment = calcTotalDebt(debtData); { uint256 newCumulativeIndex; if (deltaDebt == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( deltaDebt, // delta debt debtData.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray debtData.cumulativeIndexLastUpdate ); } // update liquidated position position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, -toInt256(takeCollateral), totalDebt); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] // transfer the collateral amount from the vault to the liquidator // cash[msg.sender] += takeCollateral; token.safeTransfer(msg.sender, takeCollateral); // Mint the penalty from the vault to the treasury // cdm.modifyBalance(address(this), address(buffer), penalty); IPoolV3Loop(address(pool)).mintProfit(penalty); }
    /// @notice Updates pool state to indicate debt repayment, can only be called by credit managers
    ///         after transferring underlying from a credit account to the pool.
    ///         - If transferred amount exceeds debt principal + base interest + quota interest,
    ///           the difference is deemed protocol's profit and the respective number of shares
    ///           is minted to the treasury.
    ///         - If, however, transferred amount is insufficient to repay debt and interest,
    ///           which may only happen during liquidation, treasury's shares are burned to
    ///           cover as much of the loss as possible.
    /// @param repaidAmount Amount of debt principal repaid
    /// @param profit Pool's profit in underlying after repaying
    /// @param loss Pool's loss in underlying after repaying
    /// @custom:expects Credit manager transfers underlying from a credit account to the pool before calling this function
    /// @custom:expects Profit/loss computed in the credit manager are cosistent with pool's implicit calculations
    function repayCreditAccount(
        uint256 repaidAmount,
        uint256 profit,
        uint256 loss
    )
        external
        override
        whenNotPaused // U:[LP-2A]
        nonReentrant // U:[LP-2B]
    {
        uint128 repaidAmountU128 = repaidAmount.toUint128();

        DebtParams storage cmDebt = _creditManagerDebt[msg.sender];
        uint128 cmBorrowed = cmDebt.borrowed;
        if (cmBorrowed == 0) {
            revert CallerNotCreditManagerException(); // U:[LP-2C,14A]
        }

        if (profit > 0) {
            _mint(treasury, convertToShares(profit)); // U:[LP-14B]
        } else if (loss > 0) {
            address treasury_ = treasury;
            uint256 sharesInTreasury = balanceOf(treasury_);
            uint256 sharesToBurn = convertToShares(loss);
            if (sharesToBurn > sharesInTreasury) {
                unchecked {
                    emit IncurUncoveredLoss({
                        creditManager: msg.sender,
                        loss: convertToAssets(sharesToBurn - sharesInTreasury)
                    }); // U:[LP-14D]
                }
                sharesToBurn = sharesInTreasury;
            }
            _burn(treasury_, sharesToBurn); // U:[LP-14C,14D]
        }

        _updateBaseInterest({
            expectedLiquidityDelta: profit.toInt256() - loss.toInt256(),
            availableLiquidityDelta: 0,
            checkOptimalBorrowing: false
        }); // U:[LP-14B,14C,14D]

        _totalDebt.borrowed -= repaidAmountU128; // U:[LP-14B,14C,14D]
        cmDebt.borrowed = cmBorrowed - repaidAmountU128; // U:[LP-14B,14C,14D]

        emit Repay(msg.sender, repaidAmount, profit, loss); // U:[LP-14B,14C,14D]
    }

    /// @notice Updates pool state to indicate debt repayment, can only be called by credit managers
    ///         after transferring underlying from a credit account to the pool.
    ///         - If transferred amount exceeds debt principal + base interest + quota interest,
    ///           the difference is deemed protocol's profit and the respective number of shares
    ///           is minted to the treasury.
    ///         - If, however, transferred amount is insufficient to repay debt and interest,
    ///           which may only happen during liquidation, treasury's shares are burned to
    ///           cover as much of the loss as possible.
    /// @param repaidAmount Amount of debt principal repaid
    /// @param profit Pool's profit in underlying after repaying
    /// @param loss Pool's loss in underlying after repaying
    /// @custom:expects Credit manager transfers underlying from a credit account to the pool before calling this function
    /// @custom:expects Profit/loss computed in the credit manager are cosistent with pool's implicit calculations
    function repayCreditAccount(
        uint256 repaidAmount,
        uint256 profit,
        uint256 loss
    )
        external
        override
        whenNotPaused // U:[LP-2A]
        nonReentrant // U:[LP-2B]
    {
        uint128 repaidAmountU128 = repaidAmount.toUint128();

        DebtParams storage cmDebt = _creditManagerDebt[msg.sender];
        uint128 cmBorrowed = cmDebt.borrowed;
        if (cmBorrowed == 0) {
            revert CallerNotCreditManagerException(); // U:[LP-2C,14A]
        }

        if (profit > 0) {
            _mint(treasury, convertToShares(profit)); // U:[LP-14B]
        } else if (loss > 0) {
            address treasury_ = treasury;
            uint256 sharesInTreasury = balanceOf(treasury_);
            uint256 sharesToBurn = convertToShares(loss);
            if (sharesToBurn > sharesInTreasury) {
                unchecked {
                    emit IncurUncoveredLoss({
                        creditManager: msg.sender,
                        loss: convertToAssets(sharesToBurn - sharesInTreasury)
                    }); // U:[LP-14D]
                }
                sharesToBurn = sharesInTreasury;
            }
            _burn(treasury_, sharesToBurn); // U:[LP-14C,14D]
        }

        _updateBaseInterest({
            expectedLiquidityDelta: profit.toInt256() - loss.toInt256(),
            availableLiquidityDelta: 0,
            checkOptimalBorrowing: false
        }); // U:[LP-14B,14C,14D]

        _totalDebt.borrowed -= repaidAmountU128; // U:[LP-14B,14C,14D]
        cmDebt.borrowed = cmBorrowed - repaidAmountU128; // U:[LP-14B,14C,14D]

        emit Repay(msg.sender, repaidAmount, profit, loss); // U:[LP-14B,14C,14D]
    }

[WP-M11] BalancerOracle doesn't work because the current implementation of chainlinkOracle doesn't support multiple tokens

BalancerOracle is expecting the ChainlinkOracle to support getting the price for a specific token.

function _getTokenPrice(uint256 index) internal view returns (uint256 price) {
    address token;
    if (index == 0) token = token0;
    else if (index == 1) token = token1;
    else if (index == 2) token = token2;
    else revert BalancerOracle__getTokenPrice_invalidIndex();

    return chainlinkOracle.spot(token);
}

However, the current implementation of ChainlinkOracle ignores the token param of spot() (see ChainlinkOracle.sol#L91) and can only support one token (ChainlinkOracle.sol#L101).

As a result, BalancerOracle cannot work as expected.

@@ 87,90 @@ /// @notice Returns the latest price for the asset from Chainlink [WAD] /// @param /*token*/ Token address /// @return price Asset price [WAD] /// @dev reverts if the price is invalid
function spot(address /* token */) external view virtual override returns (uint256 price) { bool isValid; (isValid, price) = _fetchAndValidate(); if (!isValid) revert ChainlinkOracle__spot_invalidValue(); }
@@ 97,99 @@ /// @notice Fetches and validates the latest price from Chainlink /// @return isValid Whether the price is valid based on the value range and staleness /// @return price Asset price [WAD]
function _fetchAndValidate() internal view returns (bool isValid, uint256 price) { try AggregatorV3Interface(aggregator).latestRoundData() returns ( uint80 roundId, int256 answer, uint256 /*startedAt*/, uint256 updatedAt, uint80 answeredInRound ) { isValid = (answer > 0 && answeredInRound >= roundId && block.timestamp - updatedAt <= stalePeriod); return (isValid, wdiv(uint256(answer), aggregatorScale)); } catch { // return the default values (false, 0) on failure } }
function _getTokenPrice(uint256 index) internal view returns (uint256 price) {
    address token;
    if (index == 0) token = token0;
    else if (index == 1) token = token1;
    else if (index == 2) token = token2;
    else revert BalancerOracle__getTokenPrice_invalidIndex();

    return chainlinkOracle.spot(token);
}
@@ 87,90 @@ /// @notice Returns the latest price for the asset from Chainlink [WAD] /// @param /*token*/ Token address /// @return price Asset price [WAD] /// @dev reverts if the price is invalid
function spot(address /* token */) external view virtual override returns (uint256 price) { bool isValid; (isValid, price) = _fetchAndValidate(); if (!isValid) revert ChainlinkOracle__spot_invalidValue(); }
@@ 97,99 @@ /// @notice Fetches and validates the latest price from Chainlink /// @return isValid Whether the price is valid based on the value range and staleness /// @return price Asset price [WAD]
function _fetchAndValidate() internal view returns (bool isValid, uint256 price) { try AggregatorV3Interface(aggregator).latestRoundData() returns ( uint80 roundId, int256 answer, uint256 /*startedAt*/, uint256 updatedAt, uint80 answeredInRound ) { isValid = (answer > 0 && answeredInRound >= roundId && block.timestamp - updatedAt <= stalePeriod); return (isValid, wdiv(uint256(answer), aggregatorScale)); } catch { // return the default values (false, 0) on failure } }

[WP-L12] answeredInRound is deprecated

answeredInRound is deprecated according to Chainlink's documentation.

function _fetchAndValidate() internal view returns (bool isValid, uint256 price) {
    try AggregatorV3Interface(aggregator).latestRoundData() returns (
        uint80 roundId,
        int256 answer,
        uint256 /*startedAt*/,
        uint256 updatedAt,
        uint80 answeredInRound
    ) {
        isValid = (answer > 0 && answeredInRound >= roundId && block.timestamp - updatedAt <= stalePeriod);
        return (isValid, wdiv(uint256(answer), aggregatorScale));
    } catch {
        // return the default values (false, 0) on failure
    }
}

answeredInRound: Deprecated - Previously used when answers could take multiple rounds to be computed

Ref: https://docs.chain.link/data-feeds/api-reference

Recommendation

Update _fetchAndValidate() to remove the answeredInRound check in isValid.

function _fetchAndValidate() internal view returns (bool isValid, uint256 price) {
    try AggregatorV3Interface(aggregator).latestRoundData() returns (
        uint80 roundId,
        int256 answer,
        uint256 /*startedAt*/,
        uint256 updatedAt,
        uint80 answeredInRound
    ) {
        isValid = (answer > 0 && block.timestamp - updatedAt <= stalePeriod);
        return (isValid, wdiv(uint256(answer), aggregatorScale));
    } catch {
        // return the default values (false, 0) on failure
    }
}
function _fetchAndValidate() internal view returns (bool isValid, uint256 price) {
    try AggregatorV3Interface(aggregator).latestRoundData() returns (
        uint80 roundId,
        int256 answer,
        uint256 /*startedAt*/,
        uint256 updatedAt,
        uint80 answeredInRound
    ) {
        isValid = (answer > 0 && answeredInRound >= roundId && block.timestamp - updatedAt <= stalePeriod);
        return (isValid, wdiv(uint256(answer), aggregatorScale));
    } catch {
        // return the default values (false, 0) on failure
    }
}

[WP-L13] Oracle Price Interface Fails to Validate Token, Resulting in Identical Prices for Any Token Address

If it is intended to only accept one token address, the input address should be validated.

Otherwise, it creates potential for user confusion and errors.

BalancerOracle.sol

/// @notice Returns the latest price for the asset
/// @param /*token*/ Token address
/// @return price Asset price [WAD]
/// @dev reverts if the price is invalid
function spot(address /*token*/) external view virtual override returns (uint256 price) {
    if (!_getStatus()) {
        revert BalancerOracle__spot_invalidPrice();
    }
    return safePrice;
}

ChainlinkOracle.sol

/// @notice Returns the status of the oracle
/// @param /*token*/ Token address, ignored for this oracle
/// @dev The status is valid if the price is validated and not stale
function getStatus(address /*token*/) public view virtual override returns (bool status) {
    return _getStatus();
}

/// @notice Returns the latest price for the asset from Chainlink [WAD]
/// @param /*token*/ Token address
/// @return price Asset price [WAD]
/// @dev reverts if the price is invalid
function spot(address /* token */) external view virtual override returns (uint256 price) {
    bool isValid;
    (isValid, price) = _fetchAndValidate();
    if (!isValid) revert ChainlinkOracle__spot_invalidValue();
}

Recommendation

Consider adding an assertion to verify that the token value is the expected token address.

[WP-N14] Unused function _updatePosition()

CDPVault.sol

function _updatePosition(address position) internal view returns (Position memory updatedPos) {
    Position memory pos = positions[position];
    // pos.cumulativeIndexLastUpdate =
    uint256 accruedInterest = calcAccruedInterest(
        pos.debt,
        pos.cumulativeIndexLastUpdate,
        pool.baseInterestIndex()
    );
    uint256 currentDebt = pos.debt + accruedInterest;
    uint256 spotPrice_ = spotPrice();
    uint256 collateralValue = wmul(pos.collateral, spotPrice_);

    if (spotPrice_ == 0 || _isCollateralized(currentDebt, collateralValue, vaultConfig.liquidationRatio))
        revert CDPVault__modifyCollateralAndDebt_notSafe();

    return pos;
}

[WP-G15] Using Existing Storage Local Cache to Save Gas

VaultRegistry.sol#L83 uint256 vaultLen = vaultList.length has already cached the vaultList.length storage variable value.

At VaultRegistry.sol#L86, there's no need to SLOAD vaultList.length again; vaultLen can be used directly.

@@ 80,81 @@/// @notice Removes a vault from the vaultList array /// @param vault The address of the vault to remove
function _removeVaultFromList(ICDPVault vault) private { uint256 vaultLen = vaultList.length; for (uint256 i = 0; i < vaultLen; ) { if (vaultList[i] == vault) { vaultList[i] = vaultList[vaultList.length - 1]; vaultList.pop(); break; } unchecked { ++i; } } }
@@ 80,81 @@/// @notice Removes a vault from the vaultList array /// @param vault The address of the vault to remove
function _removeVaultFromList(ICDPVault vault) private { uint256 vaultLen = vaultList.length; for (uint256 i = 0; i < vaultLen; ) { if (vaultList[i] == vault) { vaultList[i] = vaultList[vaultList.length - 1]; vaultList.pop(); break; } unchecked { ++i; } } }

[WP-O16] The Two Oracles Have Slightly Different Implementations of stalePeriod

BalancerOracle

block.timestamp - lastUpdate == stalePeriod is not allowed.

function _getStatus() internal view returns (bool status) {
    status = (safePrice != 0) && block.timestamp - lastUpdate < stalePeriod;
}

/// @notice Returns the latest price for the asset
/// @param /*token*/ Token address
/// @return price Asset price [WAD]
/// @dev reverts if the price is invalid
function spot(address /*token*/) external view virtual override returns (uint256 price) {
    if (!_getStatus()) {
        revert BalancerOracle__spot_invalidPrice();
    }
    return safePrice;
}

ChainlinkOracle

block.timestamp - updatedAt == stalePeriod is allowed.

/// @notice Returns the latest price for the asset from Chainlink [WAD]
/// @param /*token*/ Token address
/// @return price Asset price [WAD]
/// @dev reverts if the price is invalid
function spot(address /* token */) external view virtual override returns (uint256 price) {
    bool isValid;
    (isValid, price) = _fetchAndValidate();
    if (!isValid) revert ChainlinkOracle__spot_invalidValue();
}

/// @notice Fetches and validates the latest price from Chainlink
/// @return isValid Whether the price is valid based on the value range and staleness
/// @return price Asset price [WAD]
function _fetchAndValidate() internal view returns (bool isValid, uint256 price) {
    try AggregatorV3Interface(aggregator).latestRoundData() returns (
        uint80 roundId,
        int256 answer,
        uint256 /*startedAt*/,
        uint256 updatedAt,
        uint80 answeredInRound
    ) {
        isValid = (answer > 0 && answeredInRound >= roundId && block.timestamp - updatedAt <= stalePeriod);
        return (isValid, wdiv(uint256(answer), aggregatorScale));
    } catch {
        // return the default values (false, 0) on failure
    }
}
function _getStatus() internal view returns (bool status) {
    status = (safePrice != 0) && block.timestamp - lastUpdate < stalePeriod;
}

/// @notice Returns the latest price for the asset
/// @param /*token*/ Token address
/// @return price Asset price [WAD]
/// @dev reverts if the price is invalid
function spot(address /*token*/) external view virtual override returns (uint256 price) {
    if (!_getStatus()) {
        revert BalancerOracle__spot_invalidPrice();
    }
    return safePrice;
}
/// @notice Returns the latest price for the asset from Chainlink [WAD]
/// @param /*token*/ Token address
/// @return price Asset price [WAD]
/// @dev reverts if the price is invalid
function spot(address /* token */) external view virtual override returns (uint256 price) {
    bool isValid;
    (isValid, price) = _fetchAndValidate();
    if (!isValid) revert ChainlinkOracle__spot_invalidValue();
}

/// @notice Fetches and validates the latest price from Chainlink
/// @return isValid Whether the price is valid based on the value range and staleness
/// @return price Asset price [WAD]
function _fetchAndValidate() internal view returns (bool isValid, uint256 price) {
    try AggregatorV3Interface(aggregator).latestRoundData() returns (
        uint80 roundId,
        int256 answer,
        uint256 /*startedAt*/,
        uint256 updatedAt,
        uint80 answeredInRound
    ) {
        isValid = (answer > 0 && answeredInRound >= roundId && block.timestamp - updatedAt <= stalePeriod);
        return (isValid, wdiv(uint256(answer), aggregatorScale));
    } catch {
        // return the default values (false, 0) on failure
    }
}

[WP-N17] Incorrect NatSpec Documentation Comments on Collateral and Debt Amount Units

Collateral Amount Units:

  • Comments state the unit as wad:
  • Code implementation uses CDPVault.token amount units:

Debt Amount Units:

Similar issues exist in the NatSpec documentation for the amount parameter in Flashlender.flashLoan() and Flashlender.creditFlashLoan().

    // Position Accounting
    struct Position {
        uint256 collateral; // [wad]
        uint256 debt; // [wad]
        uint256 lastDebtUpdate; // [timestamp]
        uint256 cumulativeIndexLastUpdate;
    }
    /// @notice Map of user positions
    mapping(address owner => Position) public positions;
@@ 343,353 @@ /// @notice Modifies a Position's collateral and debt balances /// @dev Checks that the global debt ceiling and the vault's debt ceiling have not been exceeded via the CDM, /// - that the Position is still safe after the modification, /// - that the msg.sender has the permission of the owner to decrease the collateral-to-debt ratio, /// - that the msg.sender has the permission of the collateralizer to put up new collateral, /// - that the msg.sender has the permission of the creditor to settle debt with their credit, /// - that that the vault debt floor is exceeded /// - that the vault minimum collateralization ratio is met /// @param owner Address of the owner of the position /// @param collateralizer Address of who puts up or receives the collateral delta /// @param creditor Address of who provides or receives the credit delta for the debt delta
/// @param deltaCollateral Amount of collateral to put up (+) or to remove (-) from the position [wad] /// @param deltaDebt Amount of normalized debt (gross, before rate is applied) to generate (+) or /// to settle (-) on this position [wad] function modifyCollateralAndDebt( address owner, address collateralizer, address creditor, int256 deltaCollateral, int256 deltaDebt ) public {
@@ 364,378 @@ if ( // position is either more safe than before or msg.sender has the permission from the owner ((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) || // msg.sender has the permission of the collateralizer to collateralize the position using their cash (deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) || // msg.sender has the permission of the creditor to use their credit to repay the debt (deltaDebt < 0 && !hasPermission(creditor, msg.sender)) ) revert CDPVault__modifyCollateralAndDebt_noPermission(); Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); uint256 newDebt; uint256 newCumulativeIndex; uint256 profit;
if (deltaDebt > 0) { (newDebt, newCumulativeIndex) = calcIncrease( uint256(deltaDebt), // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); // U:[CM-10] pool.lendCreditAccount(uint256(deltaDebt), creditor); // F:[CM-20] } else if (deltaDebt < 0) { uint256 maxRepayment = calcTotalDebt(debtData); uint256 amount = abs(deltaDebt); if (amount >= maxRepayment) { amount = maxRepayment; // U:[CM-11] } poolUnderlying.safeTransferFrom(creditor, address(pool), amount); if (amount == maxRepayment) {
@@ 398,400 @@ newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees;
} else { (newDebt, newCumulativeIndex, profit) = calcDecrease( amount, // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] } if (deltaCollateral > 0) { uint256 amount = deltaCollateral.toUint256(); token.safeTransferFrom(collateralizer, address(this), amount); } else if (deltaCollateral < 0) { uint256 amount = abs(deltaCollateral); token.safeTransfer(collateralizer, amount); } position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt);
@@ 423,432 @@ VaultConfig memory config = vaultConfig; uint256 spotPrice_ = spotPrice(); uint256 collateralValue = wmul(position.collateral, spotPrice_); if ( (deltaDebt > 0 || deltaCollateral < 0) && !_isCollateralized(newDebt, collateralValue, config.liquidationRatio) ) revert CDPVault__modifyCollateralAndDebt_notSafe(); emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt);
}
    /// @notice Updates a position's collateral and debt balances
    /// @dev This is the only method which is allowed to modify a position's collateral and debt balances
    function _modifyPosition(
        address owner,
        Position memory position,
        uint256 newDebt,
        uint256 newCumulativeIndex,
        int256 deltaCollateral,
        uint256 totalDebt_
    ) internal returns (Position memory) {
        uint256 currentDebt = position.debt;
        // update collateral and debt amounts by the deltas
        position.collateral = add(position.collateral, deltaCollateral);
        position.debt = newDebt; // U:[CM-10,11]
@@ 307,331 @@ position.cumulativeIndexLastUpdate = newCumulativeIndex; // U:[CM-10,11] position.lastDebtUpdate = uint64(block.number); // U:[CM-10,11] // position either has no debt or more debt than the debt floor if (position.debt != 0 && position.debt < uint256(vaultConfig.debtFloor)) revert CDPVault__modifyPosition_debtFloor(); // store the position's balances positions[owner] = position; // update the global debt balance if (newDebt > currentDebt) { totalDebt_ = totalDebt_ + (newDebt - currentDebt); } else { totalDebt_ = totalDebt_ - (currentDebt - newDebt); } totalDebt = totalDebt_; if (address(rewardController) != address(0)) { rewardController.handleActionAfter(owner, position.debt, totalDebt_); } emit ModifyPosition(owner, position.debt, position.collateral, totalDebt_); return position;
}
@@ 465,467 @@ /// @notice Lends funds to a credit account, can only be called by credit managers /// @param borrowedAmount Amount to borrow /// @param creditAccount Credit account to send the funds to
function lendCreditAccount( uint256 borrowedAmount, address creditAccount ) external override whenNotPaused // U:[LP-2A] nonReentrant // U:[LP-2B] {
@@ 477,493 @@ uint128 borrowedAmountU128 = borrowedAmount.toUint128(); DebtParams storage cmDebt = _creditManagerDebt[msg.sender]; uint128 totalBorrowed_ = _totalDebt.borrowed + borrowedAmountU128; uint128 cmBorrowed_ = cmDebt.borrowed + borrowedAmountU128; if (borrowedAmount == 0 || cmBorrowed_ > cmDebt.limit || totalBorrowed_ > _totalDebt.limit) { revert CreditManagerCantBorrowException(); // U:[LP-2C,13A] } _updateBaseInterest({ expectedLiquidityDelta: 0, availableLiquidityDelta: -borrowedAmount.toInt256(), checkOptimalBorrowing: true }); // U:[LP-13B] cmDebt.borrowed = cmBorrowed_; // U:[LP-13B] _totalDebt.borrowed = totalBorrowed_; // U:[LP-13B]
IERC20(underlyingToken).safeTransfer({to: creditAccount, value: borrowedAmount}); // U:[LP-13B] emit Borrow(msg.sender, creditAccount, borrowedAmount); // U:[LP-13B] }
@@ 554,564 @@ /// @dev Computes new debt principal and interest index after increasing debt /// - The new debt principal is simply `debt + amount` /// - The new credit account's interest index is a solution to the equation /// `debt * (indexNow / indexLastUpdate - 1) = (debt + amount) * (indexNow / indexNew - 1)`, /// which essentially writes that interest accrued since last update remains the same /// @param amount Amount to increase debt by /// @param debt Debt principal before increase /// @param cumulativeIndexNow The current interest index /// @param cumulativeIndexLastUpdate Credit account's interest index as of last update /// @return newDebt Debt principal after increase /// @return newCumulativeIndex New credit account's interest index
function calcIncrease( uint256 amount, uint256 debt, uint256 cumulativeIndexNow, uint256 cumulativeIndexLastUpdate ) internal pure returns (uint256 newDebt, uint256 newCumulativeIndex) { if (debt == 0) return (amount, cumulativeIndexNow); newDebt = debt + amount; // U:[CL-2] newCumulativeIndex = ((cumulativeIndexNow * newDebt * INDEX_PRECISION) / ((INDEX_PRECISION * cumulativeIndexNow * debt) / cumulativeIndexLastUpdate + INDEX_PRECISION * amount)); // U:[CL-2] }
@@ 587,607 @@ /// @dev Computes new debt principal and interest index (and other values) after decreasing debt /// - Debt comprises of multiple components which are repaid in the following order: /// quota update fees => quota interest => base interest => debt principal. /// New values for all these components depend on what portion of each was repaid. /// - Debt principal, for example, only decreases if all previous components were fully repaid /// - The new credit account's interest index stays the same if base interest was not repaid at all, /// is set to the current interest index if base interest was repaid fully, and is a solution to /// the equation `debt * (indexNow / indexLastUpdate - 1) - delta = debt * (indexNow / indexNew - 1)` /// when only `delta` of accrued interest was repaid /// @param amount Amount of debt to repay /// @param debt Debt principal before repayment /// @param cumulativeIndexNow The current interest index /// @param cumulativeIndexLastUpdate Credit account's interest index as of last update // @param cumulativeQuotaInterest Credit account's quota interest before repayment // @param quotaFees Accrued quota fees // @param feeInterest Fee on accrued interest (both base and quota) charged by the DAO /// @return newDebt Debt principal after repayment /// @return newCumulativeIndex Credit account's quota interest after repayment /// @return profit Amount of underlying tokens received as fees by the DAO // @return newCumulativeQuotaInterest Credit account's accrued quota interest after repayment // @return newQuotaFees Amount of unpaid quota fees left after repayment
function calcDecrease( uint256 amount, uint256 debt, uint256 cumulativeIndexNow, uint256 cumulativeIndexLastUpdate ) internal view returns (uint256 newDebt, uint256 newCumulativeIndex, uint256 profit) { uint256 amountToRepay = amount;
@@ 616,646 @@ if (amountToRepay != 0) { uint256 interestAccrued = calcAccruedInterest({ amount: debt, cumulativeIndexLastUpdate: cumulativeIndexLastUpdate, cumulativeIndexNow: cumulativeIndexNow }); // U:[CL-3] uint256 profitFromInterest = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; // U:[CL-3] if (amountToRepay >= interestAccrued + profitFromInterest) { amountToRepay -= interestAccrued + profitFromInterest; profit += profitFromInterest; // U:[CL-3] newCumulativeIndex = cumulativeIndexNow; // U:[CL-3] } else { // If amount is not enough to repay base interest + DAO fee, then it is split pro-rata between them uint256 amountToPool = (amountToRepay * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR + feeInterest); profit += amountToRepay - amountToPool; // U:[CL-3] amountToRepay = 0; // U:[CL-3] newCumulativeIndex = (INDEX_PRECISION * cumulativeIndexNow * cumulativeIndexLastUpdate) / (INDEX_PRECISION * cumulativeIndexNow - (INDEX_PRECISION * amountToPool * cumulativeIndexLastUpdate) / debt); // U:[CL-3] } } else { newCumulativeIndex = cumulativeIndexLastUpdate; // U:[CL-3] }
newDebt = debt - amountToRepay; // U:[CL-3] }
    // Position Accounting
    struct Position {
        uint256 collateral; // [wad]
        uint256 debt; // [wad]
        uint256 lastDebtUpdate; // [timestamp]
        uint256 cumulativeIndexLastUpdate;
    }
    /// @notice Map of user positions
    mapping(address owner => Position) public positions;
@@ 343,353 @@ /// @notice Modifies a Position's collateral and debt balances /// @dev Checks that the global debt ceiling and the vault's debt ceiling have not been exceeded via the CDM, /// - that the Position is still safe after the modification, /// - that the msg.sender has the permission of the owner to decrease the collateral-to-debt ratio, /// - that the msg.sender has the permission of the collateralizer to put up new collateral, /// - that the msg.sender has the permission of the creditor to settle debt with their credit, /// - that that the vault debt floor is exceeded /// - that the vault minimum collateralization ratio is met /// @param owner Address of the owner of the position /// @param collateralizer Address of who puts up or receives the collateral delta /// @param creditor Address of who provides or receives the credit delta for the debt delta
/// @param deltaCollateral Amount of collateral to put up (+) or to remove (-) from the position [wad] /// @param deltaDebt Amount of normalized debt (gross, before rate is applied) to generate (+) or /// to settle (-) on this position [wad] function modifyCollateralAndDebt( address owner, address collateralizer, address creditor, int256 deltaCollateral, int256 deltaDebt ) public {
@@ 364,378 @@ if ( // position is either more safe than before or msg.sender has the permission from the owner ((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) || // msg.sender has the permission of the collateralizer to collateralize the position using their cash (deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) || // msg.sender has the permission of the creditor to use their credit to repay the debt (deltaDebt < 0 && !hasPermission(creditor, msg.sender)) ) revert CDPVault__modifyCollateralAndDebt_noPermission(); Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); uint256 newDebt; uint256 newCumulativeIndex; uint256 profit;
if (deltaDebt > 0) { (newDebt, newCumulativeIndex) = calcIncrease( uint256(deltaDebt), // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); // U:[CM-10] pool.lendCreditAccount(uint256(deltaDebt), creditor); // F:[CM-20] } else if (deltaDebt < 0) { uint256 maxRepayment = calcTotalDebt(debtData); uint256 amount = abs(deltaDebt); if (amount >= maxRepayment) { amount = maxRepayment; // U:[CM-11] } poolUnderlying.safeTransferFrom(creditor, address(pool), amount); if (amount == maxRepayment) {
@@ 398,400 @@ newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees;
} else { (newDebt, newCumulativeIndex, profit) = calcDecrease( amount, // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] } if (deltaCollateral > 0) { uint256 amount = deltaCollateral.toUint256(); token.safeTransferFrom(collateralizer, address(this), amount); } else if (deltaCollateral < 0) { uint256 amount = abs(deltaCollateral); token.safeTransfer(collateralizer, amount); } position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt);
@@ 423,432 @@ VaultConfig memory config = vaultConfig; uint256 spotPrice_ = spotPrice(); uint256 collateralValue = wmul(position.collateral, spotPrice_); if ( (deltaDebt > 0 || deltaCollateral < 0) && !_isCollateralized(newDebt, collateralValue, config.liquidationRatio) ) revert CDPVault__modifyCollateralAndDebt_notSafe(); emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt);
}
    /// @notice Updates a position's collateral and debt balances
    /// @dev This is the only method which is allowed to modify a position's collateral and debt balances
    function _modifyPosition(
        address owner,
        Position memory position,
        uint256 newDebt,
        uint256 newCumulativeIndex,
        int256 deltaCollateral,
        uint256 totalDebt_
    ) internal returns (Position memory) {
        uint256 currentDebt = position.debt;
        // update collateral and debt amounts by the deltas
        position.collateral = add(position.collateral, deltaCollateral);
        position.debt = newDebt; // U:[CM-10,11]
@@ 307,331 @@ position.cumulativeIndexLastUpdate = newCumulativeIndex; // U:[CM-10,11] position.lastDebtUpdate = uint64(block.number); // U:[CM-10,11] // position either has no debt or more debt than the debt floor if (position.debt != 0 && position.debt < uint256(vaultConfig.debtFloor)) revert CDPVault__modifyPosition_debtFloor(); // store the position's balances positions[owner] = position; // update the global debt balance if (newDebt > currentDebt) { totalDebt_ = totalDebt_ + (newDebt - currentDebt); } else { totalDebt_ = totalDebt_ - (currentDebt - newDebt); } totalDebt = totalDebt_; if (address(rewardController) != address(0)) { rewardController.handleActionAfter(owner, position.debt, totalDebt_); } emit ModifyPosition(owner, position.debt, position.collateral, totalDebt_); return position;
}
@@ 465,467 @@ /// @notice Lends funds to a credit account, can only be called by credit managers /// @param borrowedAmount Amount to borrow /// @param creditAccount Credit account to send the funds to
function lendCreditAccount( uint256 borrowedAmount, address creditAccount ) external override whenNotPaused // U:[LP-2A] nonReentrant // U:[LP-2B] {
@@ 477,493 @@ uint128 borrowedAmountU128 = borrowedAmount.toUint128(); DebtParams storage cmDebt = _creditManagerDebt[msg.sender]; uint128 totalBorrowed_ = _totalDebt.borrowed + borrowedAmountU128; uint128 cmBorrowed_ = cmDebt.borrowed + borrowedAmountU128; if (borrowedAmount == 0 || cmBorrowed_ > cmDebt.limit || totalBorrowed_ > _totalDebt.limit) { revert CreditManagerCantBorrowException(); // U:[LP-2C,13A] } _updateBaseInterest({ expectedLiquidityDelta: 0, availableLiquidityDelta: -borrowedAmount.toInt256(), checkOptimalBorrowing: true }); // U:[LP-13B] cmDebt.borrowed = cmBorrowed_; // U:[LP-13B] _totalDebt.borrowed = totalBorrowed_; // U:[LP-13B]
IERC20(underlyingToken).safeTransfer({to: creditAccount, value: borrowedAmount}); // U:[LP-13B] emit Borrow(msg.sender, creditAccount, borrowedAmount); // U:[LP-13B] }
@@ 554,564 @@ /// @dev Computes new debt principal and interest index after increasing debt /// - The new debt principal is simply `debt + amount` /// - The new credit account's interest index is a solution to the equation /// `debt * (indexNow / indexLastUpdate - 1) = (debt + amount) * (indexNow / indexNew - 1)`, /// which essentially writes that interest accrued since last update remains the same /// @param amount Amount to increase debt by /// @param debt Debt principal before increase /// @param cumulativeIndexNow The current interest index /// @param cumulativeIndexLastUpdate Credit account's interest index as of last update /// @return newDebt Debt principal after increase /// @return newCumulativeIndex New credit account's interest index
function calcIncrease( uint256 amount, uint256 debt, uint256 cumulativeIndexNow, uint256 cumulativeIndexLastUpdate ) internal pure returns (uint256 newDebt, uint256 newCumulativeIndex) { if (debt == 0) return (amount, cumulativeIndexNow); newDebt = debt + amount; // U:[CL-2] newCumulativeIndex = ((cumulativeIndexNow * newDebt * INDEX_PRECISION) / ((INDEX_PRECISION * cumulativeIndexNow * debt) / cumulativeIndexLastUpdate + INDEX_PRECISION * amount)); // U:[CL-2] }
@@ 587,607 @@ /// @dev Computes new debt principal and interest index (and other values) after decreasing debt /// - Debt comprises of multiple components which are repaid in the following order: /// quota update fees => quota interest => base interest => debt principal. /// New values for all these components depend on what portion of each was repaid. /// - Debt principal, for example, only decreases if all previous components were fully repaid /// - The new credit account's interest index stays the same if base interest was not repaid at all, /// is set to the current interest index if base interest was repaid fully, and is a solution to /// the equation `debt * (indexNow / indexLastUpdate - 1) - delta = debt * (indexNow / indexNew - 1)` /// when only `delta` of accrued interest was repaid /// @param amount Amount of debt to repay /// @param debt Debt principal before repayment /// @param cumulativeIndexNow The current interest index /// @param cumulativeIndexLastUpdate Credit account's interest index as of last update // @param cumulativeQuotaInterest Credit account's quota interest before repayment // @param quotaFees Accrued quota fees // @param feeInterest Fee on accrued interest (both base and quota) charged by the DAO /// @return newDebt Debt principal after repayment /// @return newCumulativeIndex Credit account's quota interest after repayment /// @return profit Amount of underlying tokens received as fees by the DAO // @return newCumulativeQuotaInterest Credit account's accrued quota interest after repayment // @return newQuotaFees Amount of unpaid quota fees left after repayment
function calcDecrease( uint256 amount, uint256 debt, uint256 cumulativeIndexNow, uint256 cumulativeIndexLastUpdate ) internal view returns (uint256 newDebt, uint256 newCumulativeIndex, uint256 profit) { uint256 amountToRepay = amount;
@@ 616,646 @@ if (amountToRepay != 0) { uint256 interestAccrued = calcAccruedInterest({ amount: debt, cumulativeIndexLastUpdate: cumulativeIndexLastUpdate, cumulativeIndexNow: cumulativeIndexNow }); // U:[CL-3] uint256 profitFromInterest = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; // U:[CL-3] if (amountToRepay >= interestAccrued + profitFromInterest) { amountToRepay -= interestAccrued + profitFromInterest; profit += profitFromInterest; // U:[CL-3] newCumulativeIndex = cumulativeIndexNow; // U:[CL-3] } else { // If amount is not enough to repay base interest + DAO fee, then it is split pro-rata between them uint256 amountToPool = (amountToRepay * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR + feeInterest); profit += amountToRepay - amountToPool; // U:[CL-3] amountToRepay = 0; // U:[CL-3] newCumulativeIndex = (INDEX_PRECISION * cumulativeIndexNow * cumulativeIndexLastUpdate) / (INDEX_PRECISION * cumulativeIndexNow - (INDEX_PRECISION * amountToPool * cumulativeIndexLastUpdate) / debt); // U:[CL-3] } } else { newCumulativeIndex = cumulativeIndexLastUpdate; // U:[CL-3] }
newDebt = debt - amountToRepay; // U:[CL-3] }

[WP-N18] The deltaDebt value in the ModifyCollateralAndDebt event differs from the -maxRepayment used in the business logic when CDPVault.modifyCollateralAndDebt() has deltaDebt < 0 && abs(deltaDebt) > maxRepayment.

This occurs because the deltaDebt variable is not updated when abs(deltaDebt) > maxRepayment. As a result, deltaDebt remains set to its original input value.

@@ 343,356 @@ /// @notice Modifies a Position's collateral and debt balances /// @dev Checks that the global debt ceiling and the vault's debt ceiling have not been exceeded via the CDM, /// - that the Position is still safe after the modification, /// - that the msg.sender has the permission of the owner to decrease the collateral-to-debt ratio, /// - that the msg.sender has the permission of the collateralizer to put up new collateral, /// - that the msg.sender has the permission of the creditor to settle debt with their credit, /// - that that the vault debt floor is exceeded /// - that the vault minimum collateralization ratio is met /// @param owner Address of the owner of the position /// @param collateralizer Address of who puts up or receives the collateral delta /// @param creditor Address of who provides or receives the credit delta for the debt delta /// @param deltaCollateral Amount of collateral to put up (+) or to remove (-) from the position [wad] /// @param deltaDebt Amount of normalized debt (gross, before rate is applied) to generate (+) or /// to settle (-) on this position [wad]
function modifyCollateralAndDebt( address owner, address collateralizer, address creditor, int256 deltaCollateral, int256 deltaDebt ) public { if ( // position is either more safe than before or msg.sender has the permission from the owner ((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) || // msg.sender has the permission of the collateralizer to collateralize the position using their cash (deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) || // msg.sender has the permission of the creditor to use their credit to repay the debt (deltaDebt < 0 && !hasPermission(creditor, msg.sender)) ) revert CDPVault__modifyCollateralAndDebt_noPermission(); Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); uint256 newDebt; uint256 newCumulativeIndex; uint256 profit; if (deltaDebt > 0) {
@@ 380,387 @@ (newDebt, newCumulativeIndex) = calcIncrease( uint256(deltaDebt), // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); // U:[CM-10] pool.lendCreditAccount(uint256(deltaDebt), creditor); // F:[CM-20]
} else if (deltaDebt < 0) { uint256 maxRepayment = calcTotalDebt(debtData); uint256 amount = abs(deltaDebt); if (amount >= maxRepayment) { amount = maxRepayment; // U:[CM-11] } poolUnderlying.safeTransferFrom(creditor, address(pool), amount); if (amount == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( amount, // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] } if (deltaCollateral > 0) { uint256 amount = deltaCollateral.toUint256(); token.safeTransferFrom(collateralizer, address(this), amount); } else if (deltaCollateral < 0) { uint256 amount = abs(deltaCollateral); token.safeTransfer(collateralizer, amount); } position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt); VaultConfig memory config = vaultConfig; uint256 spotPrice_ = spotPrice(); uint256 collateralValue = wmul(position.collateral, spotPrice_); if ( (deltaDebt > 0 || deltaCollateral < 0) && !_isCollateralized(newDebt, collateralValue, config.liquidationRatio) ) revert CDPVault__modifyCollateralAndDebt_notSafe(); emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt); }
@@ 343,356 @@ /// @notice Modifies a Position's collateral and debt balances /// @dev Checks that the global debt ceiling and the vault's debt ceiling have not been exceeded via the CDM, /// - that the Position is still safe after the modification, /// - that the msg.sender has the permission of the owner to decrease the collateral-to-debt ratio, /// - that the msg.sender has the permission of the collateralizer to put up new collateral, /// - that the msg.sender has the permission of the creditor to settle debt with their credit, /// - that that the vault debt floor is exceeded /// - that the vault minimum collateralization ratio is met /// @param owner Address of the owner of the position /// @param collateralizer Address of who puts up or receives the collateral delta /// @param creditor Address of who provides or receives the credit delta for the debt delta /// @param deltaCollateral Amount of collateral to put up (+) or to remove (-) from the position [wad] /// @param deltaDebt Amount of normalized debt (gross, before rate is applied) to generate (+) or /// to settle (-) on this position [wad]
function modifyCollateralAndDebt( address owner, address collateralizer, address creditor, int256 deltaCollateral, int256 deltaDebt ) public { if ( // position is either more safe than before or msg.sender has the permission from the owner ((deltaDebt > 0 || deltaCollateral < 0) && !hasPermission(owner, msg.sender)) || // msg.sender has the permission of the collateralizer to collateralize the position using their cash (deltaCollateral > 0 && !hasPermission(collateralizer, msg.sender)) || // msg.sender has the permission of the creditor to use their credit to repay the debt (deltaDebt < 0 && !hasPermission(creditor, msg.sender)) ) revert CDPVault__modifyCollateralAndDebt_noPermission(); Position memory position = positions[owner]; DebtData memory debtData = _calcDebt(position); uint256 newDebt; uint256 newCumulativeIndex; uint256 profit; if (deltaDebt > 0) {
@@ 380,387 @@ (newDebt, newCumulativeIndex) = calcIncrease( uint256(deltaDebt), // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); // U:[CM-10] pool.lendCreditAccount(uint256(deltaDebt), creditor); // F:[CM-20]
} else if (deltaDebt < 0) { uint256 maxRepayment = calcTotalDebt(debtData); uint256 amount = abs(deltaDebt); if (amount >= maxRepayment) { amount = maxRepayment; // U:[CM-11] } poolUnderlying.safeTransferFrom(creditor, address(pool), amount); if (amount == maxRepayment) { newDebt = 0; newCumulativeIndex = debtData.cumulativeIndexNow; profit = debtData.accruedFees; } else { (newDebt, newCumulativeIndex, profit) = calcDecrease( amount, // delta debt position.debt, debtData.cumulativeIndexNow, // current cumulative base interest index in Ray position.cumulativeIndexLastUpdate ); } pool.repayCreditAccount(debtData.debt - newDebt, profit, 0); // U:[CM-11] } if (deltaCollateral > 0) { uint256 amount = deltaCollateral.toUint256(); token.safeTransferFrom(collateralizer, address(this), amount); } else if (deltaCollateral < 0) { uint256 amount = abs(deltaCollateral); token.safeTransfer(collateralizer, amount); } position = _modifyPosition(owner, position, newDebt, newCumulativeIndex, deltaCollateral, totalDebt); VaultConfig memory config = vaultConfig; uint256 spotPrice_ = spotPrice(); uint256 collateralValue = wmul(position.collateral, spotPrice_); if ( (deltaDebt > 0 || deltaCollateral < 0) && !_isCollateralized(newDebt, collateralValue, config.liquidationRatio) ) revert CDPVault__modifyCollateralAndDebt_notSafe(); emit ModifyCollateralAndDebt(owner, collateralizer, creditor, deltaCollateral, deltaDebt); }