57907 sc high incorrect forced repayment accounting allows debt forgiveness and frees locked collateral systemic loss
Submitted on Oct 29th 2025 at 12:25:05 UTC by @edoscoba for Audit Comp | Alchemix V3
Report ID: #57907
Report Type: Smart Contract
Report severity: High
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Theft of unclaimed yield
Theft of unclaimed royalties
Description
Brief/Intro
The AlchemistV3 forced-repayment path reduces an account’s debt by a target “credit” amount in debt tokens before verifying that the account has enough MYT shares to repay the equivalent value. The actual MYT transferred is clamped to the account’s available collateral, causing the protocol to forgive debt without receiving the equivalent repayment. This also frees more locked collateral than warranted, degrading solvency and enabling systemic loss.
Vulnerability Details
In the liquidation flow, when an account is below the lower bound, the contract first attempts a “forced repayment” of earmarked debt using the account’s own MYT collateral.
Core logic (abridged) in src/AlchemistV3.sol:738:
Compute the intended debt reduction (“credit”) in debt-token units and the equivalent required MYT shares:
credit = min(amount, debt)creditToYield = convertDebtTokensToYield(credit)
Immediately reduce the account’s debt by the full
credit:_subDebt(accountId, credit)(src/AlchemistV3.sol:758)Note:
_subDebtalso frees locked collateral proportional tocredit.
Only afterwards, clamp the actual MYT repayment to the account’s available collateral:
creditToYield = min(creditToYield, account.collateralBalance)(src/AlchemistV3.sol:764)Transfer only this clamped
creditToYieldto the Transmuter (src/AlchemistV3.sol:777‒780).
Because the debt is reduced by the larger “credit” while only the smaller, clamped MYT amount is sent, the debited debt can exceed the debt-equivalent of the MYT paid. Denote:
creditDebt= the target debt reduction (in debt tokens)paidYield= actual MYT shares sent after clampingpaidDebtEq=convertYieldTokensToDebt(paidYield)
The bug is: creditDebt > paidDebtEq. This overstates repayment, frees too much locked collateral (via _subDebt), and reduces totalDebt without equivalent asset outflow.
This behavior contradicts the program’s stated design:
Program-Overview:
The only way to withdraw earmarked collateral is to repay earmarked debt with external MYT tokens.Forced repayment should never reduce debt (or free collateral) by more than the yield actually paid.
Why it’s exploitable and realistic:
It requires only that an account has earmarked debt and insufficient MYT collateral to cover the equivalent yield at current share price.
Lower MYT share prices (e.g., strategy drawdowns, withdrawal queues) make the mismatch larger because
convertDebtTokensToYield(credit)grows whileaccount.collateralBalanceis fixed and thus clamped.
Impact Details
Debt is forgiven without equivalent MYT payment. The system’s
totalDebtdecreases (and per-account debt decreases), but the Transmuter receives less MYT than required to fulfill redemptions, causing a solvency gap.Locked collateral is freed based on the over-large
credit(via_subDebt), enabling withdrawal of collateral that should remain locked, further weakening backing.Attack scenario: A borrower mints and later experiences MYT price decline; when liquidated, forced repayment removes more debt than the MYT actually paid, letting the borrower shed debt and unlock collateral at the protocol’s expense. Liquidators can repeatedly trigger this against undercollateralized, heavily earmarked accounts.
Magnitude:
One-shot loss per forced repayment equals
creditDebt - convertYieldTokensToDebt(paidYield), plus the downstream effect of prematurely reducing_totalLocked.Over multiple liquidations in volatile conditions, this can materialize as material systemic loss and potential inability to satisfy Transmuter redemptions.
Severity: High. It enables a violation of conservation (debt removed without equivalent backing outflow) and unlocks collateral improperly, risking insolvency.
References
Vulnerable logic:
src/AlchemistV3.sol:738(_forceRepay),_subDebtatsrc/AlchemistV3.sol:932.Event emitted:
ForceRepay(uint256 accountId, uint256 amount, uint256 creditToYield, uint256 protocolFeeTotal)(src/interfaces/IAlchemistV3.sol:580).Design intent: Program-Overview: “The only way to withdraw earmarked collateral is to repay earmarked debt with external MYT tokens.”
Proof of Concept
Proof of Concept
A test was added that demonstrates the mismatch. It:
Deposits MYT and mints to minimum collateralization.
Creates a Transmuter redemption equal to the account’s debt to fully earmark.
Drops MYT share price sharply so the MYT needed to repay earmarked debt exceeds the account’s available MYT.
Calls
liquidate(), which triggers_forceRepay.Captures the
ForceRepayevent and compares:evAmountDebt(debt reduced) vsconvertYieldTokensToDebt(evCreditToYield)(debt-equivalent of MYT actually sent).
Key excerpt from the test output:
ForceRepay(accountId: 1, amount: 9e19, creditToYield: 1e20, protocolFeeTotal: 0)convertYieldTokensToDebt(1e20) = 5e18Shows
evAmountDebt (9e19) >> debtFromYieldPaid (5e18)
This proves that debt is reduced by 90e18 while only 5e18 in debt-equivalent MYT was actually paid — i.e., debt forgiveness occurred. The test is included as testDebtForgivenessOnForcedRepayment() in src/test/AlchemistV3.t.sol and passes with:
forge test -m testDebtForgivenessOnForcedRepayment
import this also : import {Vm} from "../../lib/forge-std/src/Vm.sol";.
Was this helpful?