57308 sc high alchemistv3 does not update mytsharesdeposited when performing liquidation causing global accounting and liquidation logic mismatch
Description
Brief/Intro
Vulnerability Details
// AlchemistV3::_liquidate()
function _liquidate(uint256 accountId) internal returns (uint256 amountLiquidated, uint256 feeInYield, uint256 feeInUnderlying) {
// Query transmuter and earmark global debt
_earmark();
// Sync current user debt before deciding how much needs to be liquidated
_sync(accountId);
Account storage account = _accounts[accountId];
// Early return if no debt exists
if (account.debt == 0) {
return (0, 0, 0);
}
// In the rare scenario where 1 share is worth 0 underlying asset
if (IVaultV2(myt).convertToAssets(1e18) == 0) {
return (0, 0, 0);
}
// Calculate initial collateralization ratio
uint256 collateralInUnderlying = totalValue(accountId);
uint256 collateralizationRatio = collateralInUnderlying * FIXED_POINT_SCALAR / account.debt;
// If account is healthy, nothing to liquidate
if (collateralizationRatio > collateralizationLowerBound) {
return (0, 0, 0);
}
// Try to repay earmarked debt if it exists
uint256 repaidAmountInYield = 0;
if (account.earmarked > 0) {
@> repaidAmountInYield = _forceRepay(accountId, account.earmarked);
}
// If debt is fully cleared, return with only the repaid amount, no liquidation needed, caller receives repayment fee
if (account.debt == 0) {
feeInYield = _resolveRepaymentFee(accountId, repaidAmountInYield);
TokenUtils.safeTransfer(myt, msg.sender, feeInYield);
return (repaidAmountInYield, feeInYield, 0);
}
// Recalculate ratio after any repayment to determine if further liquidation is needed
collateralInUnderlying = totalValue(accountId);
collateralizationRatio = collateralInUnderlying * FIXED_POINT_SCALAR / account.debt;
if (collateralizationRatio <= collateralizationLowerBound) {
// Do actual liquidation
@> return _doLiquidation(accountId, collateralInUnderlying, repaidAmountInYield);
} else {
// Since only a repayment happened, send repayment fee to caller
feeInYield = _resolveRepaymentFee(accountId, repaidAmountInYield);
TokenUtils.safeTransfer(myt, msg.sender, feeInYield);
return (repaidAmountInYield, feeInYield, 0);
}
}
// AlchemistV3::_resolveRepaymentFee()
function _resolveRepaymentFee(uint256 accountId, uint256 repaidAmountInYield) internal returns (uint256 fee) {
Account storage account = _accounts[accountId];
// calculate repayment fee and deduct from account
fee = repaidAmountInYield * repaymentFee / BPS;
@>1 account.collateralBalance -= fee > account.collateralBalance ? account.collateralBalance : fee;
emit RepaymentFee(accountId, repaidAmountInYield, msg.sender, fee);
return fee;
}
// AlchemistV3::_doLiquidation()
function _doLiquidation(uint256 accountId, uint256 collateralInUnderlying, uint256 repaidAmountInYield)
internal
returns (uint256 amountLiquidated, uint256 feeInYield, uint256 feeInUnderlying)
{
Account storage account = _accounts[accountId];
(uint256 liquidationAmount, uint256 debtToBurn, uint256 baseFee, uint256 outsourcedFee) = calculateLiquidation(
collateralInUnderlying,
account.debt,
minimumCollateralization,
@>2 normalizeUnderlyingTokensToDebt(_getTotalUnderlyingValue()) * FIXED_POINT_SCALAR / totalDebt,
globalMinimumCollateralization,
liquidatorFee
);
amountLiquidated = convertDebtTokensToYield(liquidationAmount);
feeInYield = convertDebtTokensToYield(baseFee);
// update user balance and debt
@>1 account.collateralBalance = account.collateralBalance > amountLiquidated ? account.collateralBalance - amountLiquidated : 0;
_subDebt(accountId, debtToBurn);
// send liquidation amount - fee to transmuter
TokenUtils.safeTransfer(myt, transmuter, amountLiquidated - feeInYield);
// send base fee to liquidator if available
if (feeInYield > 0 && account.collateralBalance >= feeInYield) {
TokenUtils.safeTransfer(myt, msg.sender, feeInYield);
}
// Handle outsourced fee from vault
if (outsourcedFee > 0) {
uint256 vaultBalance = IFeeVault(alchemistFeeVault).totalDeposits();
if (vaultBalance > 0) {
uint256 feeBonus = normalizeDebtTokensToUnderlying(outsourcedFee);
feeInUnderlying = vaultBalance > feeBonus ? feeBonus : vaultBalance;
IFeeVault(alchemistFeeVault).withdraw(msg.sender, feeInUnderlying);
}
}
emit Liquidated(accountId, msg.sender, amountLiquidated + repaidAmountInYield, feeInYield, feeInUnderlying);
return (amountLiquidated + repaidAmountInYield, feeInYield, feeInUnderlying);
}
// AlchemistV3::_forceRepay(
function _forceRepay(uint256 accountId, uint256 amount) internal returns (uint256) {
if (amount == 0) {
return 0;
}
_checkForValidAccountId(accountId);
Account storage account = _accounts[accountId];
// Query transmuter and earmark global debt
_earmark();
// Sync current user debt before deciding how much is available to be repaid
_sync(accountId);
uint256 debt;
// Burning yieldTokens will pay off all types of debt
_checkState((debt = account.debt) > 0);
uint256 credit = amount > debt ? debt : amount;
uint256 creditToYield = convertDebtTokensToYield(credit);
_subDebt(accountId, credit);
// Repay debt from earmarked amount of debt first
uint256 earmarkToRemove = credit > account.earmarked ? account.earmarked : credit;
account.earmarked -= earmarkToRemove;
creditToYield = creditToYield > account.collateralBalance ? account.collateralBalance : creditToYield;
@>1 account.collateralBalance -= creditToYield;
uint256 protocolFeeTotal = creditToYield * protocolFee / BPS;
emit ForceRepay(accountId, amount, creditToYield, protocolFeeTotal);
if (account.collateralBalance > protocolFeeTotal) {
@>1 account.collateralBalance -= protocolFeeTotal;
// Transfer the protocol fee to the protocol fee receiver
TokenUtils.safeTransfer(myt, protocolFeeReceiver, protocolFeeTotal);
}
if (creditToYield > 0) {
// Transfer the repaid tokens from the account to the transmuter.
TokenUtils.safeTransfer(myt, address(transmuter), creditToYield);
}
return creditToYield;
}
// AlchemistV3::calculateLiquidation()
function calculateLiquidation(
uint256 collateral,
uint256 debt,
uint256 targetCollateralization,
uint256 alchemistCurrentCollateralization,
uint256 alchemistMinimumCollateralization,
uint256 feeBps
) public pure returns (uint256 grossCollateralToSeize, uint256 debtToBurn, uint256 fee, uint256 outsourcedFee) {
if (debt >= collateral) {
outsourcedFee = (debt * feeBps) / BPS;
// fully liquidate debt if debt is greater than collateral
return (collateral, debt, 0, outsourcedFee);
}
@>2 if (alchemistCurrentCollateralization < alchemistMinimumCollateralization) {
outsourcedFee = (debt * feeBps) / BPS;
// fully liquidate debt in high ltv global environment
return (debt, debt, 0, outsourcedFee);
}
// SNIP...
}Impact Details
References
Proof of Concept
Proof of Concept
Previous58573 sc critical alchemistv3 repayment fee cross account theft vulnerabilityNext57599 sc low protocol wrongly withdraws before checking balance of withdraw
Was this helpful?