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.
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);
}
/// @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);
}
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;
}
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;
}
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);
}
PoolV3.sol
Interest Cannot Be Distributed to Equity HoldersNo code for interest accounting was found.
Instead, we noticed that the share price used in PoolV3
deposit and withdrawal operations is always .
@@ 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);
}
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 @@
}
lastBaseInterestUpdate
results in recalculation of _baseInterestIndexLU
on every call, leading to excessive interest accrualDue 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);
}
Update lastBaseInterestUpdate
at PoolV3.sol#L631:
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);
}
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]
}
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]
}
PositionAction20.deposit()
and PositionAction4626.deposit()
will revert due to unexpected double execution of modifyCollateralAndDebt()
deposit()
PositionAction.sol#L190 -> _deposit()
PositionAction.sol#L553 -> _onDeposit()
PositionAction20.sol#L41 / PositionAction4626.sol#L51 -> CDPVault.deposit()
CDPVault.sol#L232-238 calls modifyCollateralAndDebt()
deposit()
deposit()
PositionAction.sol#L191 calls ICDPVault(vault).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
});
}
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)
}
}
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.
@@ 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]
}
chainlinkOracle
doesn't support multiple tokensBalancerOracle 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
}
}
answeredInRound
is deprecatedansweredInRound 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
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
}
}
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.
/// @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 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();
}
Consider adding an assertion to verify that the token
value is the expected token address.
_updatePosition()
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;
}
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;
}
}
}
stalePeriod
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;
}
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
}
}
Collateral Amount Units:
wad
:
deltaCollateral
, CDPVault.sol#L112 comment for position.collateral
specify the unit as wad
CDPVault.token
amount units:
deltaCollateral
as CDPVault.token
amountdeltaCollateral
to position.collateral
position.collateral
and deltaCollateral
units match CDPVault.token
amount units, depending on CDPVault.token.decimals()
Debt Amount Units:
wad
:
deltaDebt
, CDPVault.sol#L113 comment for position.debt
specify the unit as wad
CDPVault.pool.underlyingToken
amount units:
deltaDebt
as CDPVault.pool.underlyingToken
amountdeltaDebt
to position.debt
position.debt
and deltaDebt
units match CDPVault.pool.underlyingToken
amount units, depending on CDPVault.pool.underlyingToken.decimals()
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]
}
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);
}