Alchemix alETH Vault-Index Mismatch / Harvest Debt-Erasure Incident (2021-06-16)

  • Name: Alchemix alETH vault-index mismatch / harvest debt-erasure incident
  • Date: 2021-06-16
  • Protocol: Alchemix
  • Chain: Ethereum
  • Category: deployment / initialization mistake / vault-index accounting mismatch / false-yield debt erasure
  • Loss: roughly 2,700 ETH net undercollateralization (about $6.5M at the time) as publicly described in early writeups, after a mistaken harvest path treated about 4,308.1449 WETH as distributable yield
  • Summary: During Alchemix’s first alETH rollout, the deployment process created extra vault entries on the transmuter side. Deposit accounting was then recorded on the last vault entry via _vaults.last(), but harvest logic later ran against _vaultId = 0. Because vault 0 had totalDeposited = 0, the transmuter-side harvest path treated a large WETH balance as pure profit and forwarded that value into the AlchemistEth adapter. A later harvest(0) on the AlchemistEth side saw roughly 4,308.1449 WETH of apparent yield, distributed it through _distributeToTransmuter(), and called alETH.lowerHasMinted(...), effectively writing down borrower debt across the alETH system. Borrowers could then withdraw ETH collateral without repaying the alETH they had minted.
  • Trigger / entry point:
    • Alchemix launched alETH on 2021-06-12 with a new ETH/WETH-flavored Alchemist, Transmuter, and Yearn-linked adapter stack.
    • The postmortem summarized by SlowMist says the alETH deployment script accidentally created three vaults in the transmuter path instead of one.
    • Transmuter-side accounting used the latest vault entry for deposits, but harvest later referenced vault index 0, so the protocol’s what vault owns this balance? assumption split apart.
    • Once the wrong vault was harvested, the resulting WETH moved into the AlchemistEth adapter and was later recognized as real yield available to repay alETH debt.
  • Root cause:
    • This was not an attacker-manipulated market price or forged oracle. It was protocol-authored accounting fiction caused by a rollout / initialization mistake.
    • Two coupled modules used the same vault array concept but disagreed about which concrete vault entry held the live alETH deployment’s deposits and yield.
    • Harvest logic trusted vaultId-selected totalDeposited as the denominator for what part of adapter value is principal versus profit?; after the bad rollout, that assumption was false for vault 0.
    • Once the wrong-side value was misclassified as profit, the rest of the system handled it exactly as designed: yield was distributed to the transmuter and alETH.lowerHasMinted() reduced borrower debt.
  • Exploit path:
    1. Alchemix launched alETH with contracts including alETH 0x0100546F2cD4C9D97f798fFC9755E47865FF7Ee6, AlchemistEth 0x6B566554378477490ab040f6F757171c967D03ab, AlchemistEth Vault Adapter 0xEBA649E0010818Aa4321088D34bD6162d65E7971, TransmuterEth 0x45f81eF5F2ae78f49851f7A62e4061FF54Ff674B, and TransmuterEth Vault Adapter 0x54dc35eb8c2E2E20f3657Af6F84cd9949C08CF38.
    2. According to the incident analysis later summarized by SlowMist, the deployment process added extra transmuter vault entries, so live deposits accumulated on the last vault entry while subsequent harvest logic still used vault 0.
    3. When the wrong transmuter vault was harvested, its totalDeposited was effectively 0, so about 4,308.144937764982866415 WETH was treated as harvestable profit and sent onward into the adapter / repayment flow instead of being recognized as user principal.
    4. In transaction 0x3cc071f9f40294bb250fc7b9aa6b2d7e6ca5707ce4d6d222157d7a0feef618b3, AlchemistEth.harvest(0) returned values corresponding to roughly 4,308.1449 WETH harvested / decreased value, and the contract’s _distributeToTransmuter() path forwarded the amount to the transmuter while calling alETH.lowerHasMinted(amount).
    5. That mistaken debt write-down made outstanding alETH borrows look repaid even though the corresponding ETH collateral had not actually earned that much legitimate yield.
    6. Borrowers could then withdraw their ETH collateral and also keep or sell the already-minted alETH, leaving the system undercollateralized.
  • What the attacker needed to be true:
    • The bad alETH rollout had to leave live value associated with one vault entry while harvest and accounting looked at another.
    • The wrong harvested balance had to be large enough that debt reduction across borrowers became material.
    • The debt-reduction path had to keep trusting internally authored harvest output, because the protocol itself was the source of the false profit signal.
    • Borrowers had to be able to act before the alETH mint / borrow system was paused.
  • Technical anatomy worth remembering:
    • The vulnerable seam was which vault entry is canonical?, not a missing access control check or a manipulable AMM quote.
    • Vault.harvest() defines profit as totalValue() - totalDeposited; once totalDeposited was read from the wrong vault object, ordinary adapter balances were reinterpreted as yield.
    • Alchemist._distributeToTransmuter() was only three lines in the deployed code path: approve the transmuter, call distribute(address(this), amount), then call xtoken.lowerHasMinted(amount). That means any false-positive harvest immediately became borrower debt relief.
    • The incident is a good reminder that harvest(), sync(), and similar bookkeeping verbs are publication-authority surfaces. When they misclassify balance-sheet state, downstream debt logic treats the result as canonical money truth.
    • This was a reverse-rug style failure: users were not having collateral stolen directly by a reentrant or forged call; the protocol first forgave debt it should still have been owed, and users then extracted the collateral honestly against the broken ledger.
  • Representative transactions and addresses:
    • Public incident acknowledgement: Alchemix statement on X, 2021-06-16 (There has been an incident with the Alchemix alETH contracts ... Funds are safe.)
    • Main publicly cited harvest / debt-write-down tx: 0x3cc071f9f40294bb250fc7b9aa6b2d7e6ca5707ce4d6d222157d7a0feef618b3
    • alETH: 0x0100546F2cD4C9D97f798fFC9755E47865FF7Ee6
    • AlchemistEth: 0x6B566554378477490ab040f6F757171c967D03ab
    • AlchemistEth Vault Adapter: 0xEBA649E0010818Aa4321088D34bD6162d65E7971
    • TransmuterEth: 0x45f81eF5F2ae78f49851f7A62e4061FF54Ff674B
    • TransmuterEth Vault Adapter: 0x54dc35eb8c2E2E20f3657Af6F84cd9949C08CF38
  • Response / mitigation:
    • Alchemix publicly acknowledged the incident the same day, said funds were safe, and paused new alETH minting while it worked with Yearn on a postmortem and fix.
    • The team published an incident report and later appealed to users to return the accidentally freed ETH / alETH so the protocol could restore solvency.
    • Public postmortems converged on the same immediate lesson: deployment / initialization scripts for balance-sheet systems need the same adversarial review as the contracts themselves, especially where array indices, active-vault pointers, or last()-style helpers define economic truth.

Three debt-publication promotions this incident collapsed

  1. A vault-shaped accounting object still existed somewhere in the rollout

    • The first dangerous fact was not yet profit was fake. It was that the rollout had already created multiple vault entries, so the system no longer had one unquestioned answer to which vault object currently owns live alETH principal?
    • Deposits were landing on the latest vault entry while older vault slots still existed as harvest-addressable objects.
  2. The wrong vault object was still promoted into canonical profit truth

    • Vault.harvest() defined profit as totalValue() - totalDeposited.
    • Once harvest(0) ran against a vault object whose totalDeposited was effectively zero, ordinary WETH balance was reinterpreted as yield instead of principal, even though the real deposits lived on a different vault entry.
    • This was the decisive publication step: internally authored bookkeeping fiction became the protocol’s official answer to how much alETH debt can now be written down?
  3. That published profit truth still deserved immediate debt-erasure and withdrawal authority

    • After harvest labeled about 4,308.1449 WETH as distributable profit, downstream Alchemist logic socialized it through the transmuter path and alETH.lowerHasMinted(...).
    • Borrowers then inherited real balance-sheet consequences from that false publication: their debt looked repaid enough to withdraw ETH collateral through otherwise ordinary exit flows.

The durable lesson is that a vault object exists, this specific vault object still defines canonical principal-versus-profit truth, and that published harvest result still deserves debt-relief authority are three different promotions. Alchemix’s rollout mistake became a solvency event because the later bookkeeping path silently approved all three.

Pattern tags

  • deployment-time extra vault creation split which vault holds principal? away from which vault will harvest?, so one rollout mistake created two competing accounting truths
  • vault-local totalDeposited served as the denominator for principal vs profit, making wrong-vault selection a direct yield-fabrication primitive rather than a harmless indexing bug
  • harvest() had publication authority over debt relief: once misclassified adapter value was labeled profit, downstream code immediately socialized it through transmuter distribution and lowerHasMinted(...)
  • the monetization path was ledger-first, withdrawal-second: the protocol forgave debt internally before users extracted ETH collateral through otherwise ordinary withdraw flows

Why the exploit worked

  • Alchemix’s modules stopped agreeing on the one canonical vault object that represented real alETH principal. Deposits accumulated on the last transmuter vault entry, but later harvest logic still asked vault 0 to explain the system’s balance sheet.
  • That made the profit test lie mechanically. totalValue - totalDeposited only works if totalDeposited belongs to the same vault object as the assets being evaluated; with vault 0 showing effectively zero principal, ordinary WETH balance became apparent yield.
  • The dangerous authority boundary sat in harvest(), not only in the rollout script. Once harvest(0) published the misclassified WETH as profit, downstream code treated that number as canonical debt-repayment truth and propagated it through _distributeToTransmuter() and alETH.lowerHasMinted(...) without an independent solvency cross-check.
  • No sophisticated market manipulation was needed after that. The protocol itself authored the false repayment state, so borrowers could realize value simply by withdrawing collateral against a ledger that now claimed their alETH debt had already been reduced.
  • This is why the incident belongs in the accounting-publication family rather than just the deployment-mistake bucket: the rollout bug created the bad state, but the exploit paid because a bookkeeping path was authorized to crystallize that bad state into systemwide money truth.

Detection and audit pivots

  • Diff every which vault is canonical? read and write across deployment scripts, adapters, harvest paths, and debt-repayment flows. If deposits land on _vaults.last() while harvest or repayment still references vaultId = 0, the protocol has already split principal truth from yield truth.
  • Treat harvest(), sync(), and distribute() outputs as solvency publications, not maintenance events. Review whether any one misindexed vault object can turn ordinary adapter balance into apparent profit and then directly call debt-reduction logic such as lowerHasMinted(...).
  • Alert on impossible yield shapes: a vault with totalDeposited == 0 or near-zero principal should not suddenly report large harvestable value, especially when the adapter or transmuter side is visibly holding user principal.
  • Reconcile module-local principal accounting against downstream debt changes. Large borrower debt reductions or transmuter distributions should be explainable by real strategy profit, not merely by internal balance movement between adapters and vault wrappers.
  • Exercise deployment and migration scripts with invariant tests that name concrete vault IDs, lengths, and active-vault pointers before mainnet rollout. Extra vault entry exists is not a harmless indexing bug when later accounting treats array position as money truth.
  • Review emergency pause and route-quarantine policy around bookkeeping surfaces too. If harvest() or similar accounting publication paths can forgive debt system-wide, they deserve the same monitoring, pause authority, and change review as mint or withdraw entrypoints.

Failure-prevention checklist

  • Make one vault identity canonical across deployment, deposit, harvest, and debt-accounting paths. If one module writes deposits to _vaults.last() while another hard-codes or defaults to vaultId = 0, fail deployment rather than trusting post-launch operator discipline.
  • Treat deployment scripts and initialization helpers as balance-sheet code. Assert vault-count, active-vault pointer, adapter wiring, and transmuter/alchemist index agreement onchain before minting or borrowing is enabled.
  • Refuse to recognize profit from a vault object whose recorded principal is zero or implausibly low relative to the balances sitting behind the adapter or transmuter path. zero principal + large harvest should be a hard stop, not a windfall.
  • Separate harvest observation from debt-relief activation. A fresh harvest() result should not immediately flow into distribute(...) and lowerHasMinted(...) without an independent reconciliation that the recognized yield came from the correct principal-bearing vault.
  • Reconcile module-local principal and downstream debt changes continuously. If a harvest claims thousands of WETH of yield, the protocol should be able to point to the exact strategy gains, vault principal base, and borrower-debt reduction that justify it.
  • Add invariant tests for wrong vault selected and extra vault exists cases, not only happy-path one vault deployments. A topology mistake that changes which object defines totalDeposited is a solvency failure, not a cosmetic indexing issue.
  • Put pause / quarantine controls around accounting-publication surfaces like harvest() and transmuter distribution, because those paths can forgive debt or socialize losses system-wide just as directly as mint and withdraw paths.
  • Require post-deployment dry runs on production-like state before enabling borrows: deposit, harvest, debt repayment, and withdraw should all point at the same canonical vault object and preserve collateralization under adversarial indexing assumptions.

Audit prompts

  • What exact object defines principal for this vault family, and do every deposit, harvest, sync, and debt-repayment path reference that same object rather than an array position that can drift at deployment time?
  • If the protocol creates more than one vault entry by mistake, what invariant or runtime check prevents later bookkeeping from harvesting the wrong one?
  • Can a harvest() path publish profit from a vault whose totalDeposited is zero, stale, or obviously inconsistent with the balances sitting in adapters, strategies, or transmuters?
  • Where does the system prove that recognized yield came from real strategy appreciation rather than from principal being attached to one object while profit = totalValue - totalDeposited is evaluated on another?
  • Which bookkeeping helpers can immediately change borrower debt, share value, or withdrawal rights, and what independent cross-check stops a false-positive publication from becoming canonical solvency truth?
  • Are deployment scripts tested as adversarial inputs? Specifically: can extra vault creation, wrong active-vault selection, or last() / index mismatches survive into production because the script layer is trusted more than the contract layer deserves?
  • If a large debt write-down lands unexpectedly, what telemetry explains it in terms of real yield source, vault identity, and strategy performance before users are allowed to withdraw collateral against it?
  • Can operators pause harvest-side debt recognition separately from deposits and withdrawals, or does the protocol only discover that bookkeeping was root authority after undercollateralization already exists?

Lessons that should already be standard

  • Deployment topology is part of the balance sheet. An extra vault entry or wrong active pointer can be economically equivalent to publishing a fake oracle price if later code treats array position as money truth.
  • Harvest is not harmless maintenance when downstream code uses it to forgive debt. A bookkeeping verb that can reduce liabilities is already a solvency-critical publication surface.
  • Wrong-object selection is enough to manufacture profit. If principal and value come from different vault objects, ordinary balances will eventually be reinterpreted as yield.
  • Internal protocol fiction can be more dangerous than external market manipulation because every downstream module is predisposed to trust protocol-authored state.
  • Rollout scripts deserve adversarial review. If contracts are precise but initialization chooses the wrong canonical object, the system can still mint, forgive, or withdraw against false accounting.
  • Debt-erasure incidents are often ledger-first and cash-out-second: the protocol writes down liabilities incorrectly, and users then extract value through otherwise legitimate withdrawal flows.
  • Funds are safe can be technically true while the ledger is already broken. Solvency depends on whether the protocol’s published accounting truth is still anchored to the right principal object, not only on whether assets remain in contract.

Why this incident matters

  • Alchemix preserved a less common but very important exploit family: the protocol can manufacture its own false yield and then spend that fiction through otherwise-correct debt-accounting logic.
  • The incident sits near oracle / vault cases in effect, but the key mistake came earlier than price publication. The accounting object that defined principal vs profit was already wrong.
  • It is also a strong early exemplar for treating harvest and bookkeeping functions as part of the solvency boundary rather than as harmless maintenance.

Internal linkages

Control surface