56740 sc critical unbounded liquidation fee allows theft of shared collateral
Submitted on Oct 20th 2025 at 10:11:14 UTC by @godwinudo for Audit Comp | Alchemix V3
Report ID: #56740
Report Type: Smart Contract
Report severity: Critical
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
Description
Brief/Intro
The _liquidate() function contains an issue in its repayment fee logic that allows liquidators to receive more MYT tokens than the liquidated account actually possessed. When a position with fully earmarked debt is liquidated and the account has insufficient collateral remaining after force repayment, the system transfers an uncapped repayment fee to the liquidator. This fee is sourced from the shared collateral pool, effectively stealing MYT tokens from other innocent users' deposits. This occurs during normal protocol operations when transmuter positions mature after their standard 61-day term.
Vulnerability Details
To understand why this issue exists, we must first examine how the Transmuter creates the vulnerable state.
The Alchemix Transmuter is a core protocol feature that allows users to redeem alAssets 1:1 for underlying assets after a fixed waiting period. This is the protocol's primary mechanism for fixed-rate yield and peg maintenance.
When a user deposits alAssets into the Transmuter, they create a redemption position with a standard maturation period:
timeToTransmute = 5,256,000 blocks, equals approximately 61 days at 12 seconds per block. This is the standard redemption term set by the protocol for all transmuter positions.
As transmuter positions progress toward maturity, the Alchemist protocol linearly earmarks the corresponding borrower debt. This earmarking is handled automatically in the _earmark() function, which is called at the start of every liquidation:
The queryGraph() function in the Transmuter returns how much debt should be earmarked based on the linear progression of all transmuter positions:
For a transmuter position with amount D and term T blocks, the earmarking rate is linear:
Earmark rate per block =
D / TAfter
Tblocks elapsed: Total earmarked =(D / T) × T = D
For a realistic example with 180,000 alUSD debt:
Transmutation term: 5,256,000 blocks (standard 61 days)
Earmark per block: 180,000 / 5,256,000 ≈ 0.0342 alUSD
After 5,256,000 blocks: 0.0342 × 5,256,000 = 180,000 alUSD (100% earmarked)
The issue arises during full earmarking, when _resolveRepaymentFee() returns an uncapped fee amount while only capping the deduction from the account's collateral balance.
When a position becomes liquidatable and has full earmarked debt, the _liquidate() function attempts to force repay that debt first:
The critical path is when account.debt == 0 after the force repayment. At this point, the function calculates a repayment fee and transfers it to the liquidator.
The _forceRepay() function transfers the repaid amount to the transmuter and attempts to collect a protocol fee:
Notice that when account.collateralBalance <= protocolFeeTotal, the protocol fee is not deducted or transferred. This leaves the account with a minimal or zero collateral balance.
This is where the main issue exists._resolveRepaymentFee() returns an uncapped fee
The function calculates fee = repaidAmountInYield * repaymentFee / BPS. For example, with a 10% repayment fee and 190K MYT repaid, the fee is 19K MYT.
The line account.collateralBalance -= fee > account.collateralBalance ? account.collateralBalance : fee deducts the MINIMUM of (fee, account.collateralBalance) from the account. If the account has 0 balance remaining, it deducts 0.
However, the function returns fee - the full uncapped 19K MYT, not the amount actually deducted from the account.
Back in _liquidate(), the uncapped fee is transferred
The TokenUtils.safeTransfer(myt, msg.sender, feeInYield) call transfers the full uncapped fee amount from the Alchemist contract's balance. Since the contract holds a shared pool of all users' collateral, this transfer effectively steals MYT from innocent users who have nothing to do with the liquidated position.
Impact Details
Liquidators can receive more MYT than the liquidated account actually owns, and the excess is automatically taken from the collective collateral pool, draining funds from innocent users. Every user who deposits collateral into the Alchemist contract is at risk. The stolen funds come from the shared collateral pool.
Proof of Concept
Proof of Concept
Add this to the AlchemistV3.t.sol test
Was this helpful?