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 ETHnet undercollateralization (about$6.5Mat the time) as publicly described in early writeups, after a mistaken harvest path treated about4,308.1449 WETHas 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 vault0hadtotalDeposited = 0, the transmuter-side harvest path treated a large WETH balance as pure profit and forwarded that value into the AlchemistEth adapter. A laterharvest(0)on the AlchemistEth side saw roughly4,308.1449 WETHof apparent yield, distributed it through_distributeToTransmuter(), and calledalETH.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’swhat 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-selectedtotalDepositedas the denominator forwhat part of adapter value is principal versus profit?; after the bad rollout, that assumption was false for vault0. - 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:
- Alchemix launched alETH with contracts including
alETH0x0100546F2cD4C9D97f798fFC9755E47865FF7Ee6,AlchemistEth0x6B566554378477490ab040f6F757171c967D03ab,AlchemistEth Vault Adapter0xEBA649E0010818Aa4321088D34bD6162d65E7971,TransmuterEth0x45f81eF5F2ae78f49851f7A62e4061FF54Ff674B, andTransmuterEth Vault Adapter0x54dc35eb8c2E2E20f3657Af6F84cd9949C08CF38. - 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. - When the wrong transmuter vault was harvested, its
totalDepositedwas effectively0, so about4,308.144937764982866415 WETHwas treated as harvestable profit and sent onward into the adapter / repayment flow instead of being recognized as user principal. - In transaction
0x3cc071f9f40294bb250fc7b9aa6b2d7e6ca5707ce4d6d222157d7a0feef618b3,AlchemistEth.harvest(0)returned values corresponding to roughly4,308.1449 WETHharvested / decreased value, and the contract’s_distributeToTransmuter()path forwarded the amount to the transmuter while callingalETH.lowerHasMinted(amount). - 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.
- Borrowers could then withdraw their ETH collateral and also keep or sell the already-minted alETH, leaving the system undercollateralized.
- Alchemix launched alETH with contracts including
- 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
harvestoutput, 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 astotalValue() - totalDeposited; oncetotalDepositedwas 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, calldistribute(address(this), amount), then callxtoken.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.
- The vulnerable seam was
- 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:0x0100546F2cD4C9D97f798fFC9755E47865FF7Ee6AlchemistEth:0x6B566554378477490ab040f6F757171c967D03abAlchemistEth Vault Adapter:0xEBA649E0010818Aa4321088D34bD6162d65E7971TransmuterEth:0x45f81eF5F2ae78f49851f7A62e4061FF54Ff674BTransmuterEth Vault Adapter:0x54dc35eb8c2E2E20f3657Af6F84cd9949C08CF38
- Public incident acknowledgement: Alchemix statement on X, 2021-06-16 (
- 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
-
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 towhich 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.
- The first dangerous fact was not yet
-
The wrong vault object was still promoted into canonical profit truth
Vault.harvest()defined profit astotalValue() - totalDeposited.- Once
harvest(0)ran against a vault object whosetotalDepositedwas 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?
-
That published profit truth still deserved immediate debt-erasure and withdrawal authority
- After harvest labeled about
4,308.1449 WETHas distributable profit, downstream Alchemist logic socialized it through the transmuter path andalETH.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.
- After harvest labeled about
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 fromwhich vault will harvest?, so one rollout mistake created two competing accounting truths - vault-local
totalDepositedserved as the denominator forprincipal 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 andlowerHasMinted(...)- 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
0to explain the system’s balance sheet. - That made the profit test lie mechanically.
totalValue - totalDepositedonly works iftotalDepositedbelongs to the same vault object as the assets being evaluated; withvault 0showing effectively zero principal, ordinary WETH balance became apparent yield. - The dangerous authority boundary sat in
harvest(), not only in the rollout script. Onceharvest(0)published the misclassified WETH as profit, downstream code treated that number as canonical debt-repayment truth and propagated it through_distributeToTransmuter()andalETH.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 referencesvaultId = 0, the protocol has already split principal truth from yield truth. - Treat
harvest(),sync(), anddistribute()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 aslowerHasMinted(...). - Alert on impossible yield shapes: a vault with
totalDeposited == 0or 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 existsis 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 tovaultId = 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 harvestshould be a hard stop, not a windfall. - Separate harvest observation from debt-relief activation. A fresh
harvest()result should not immediately flow intodistribute(...)andlowerHasMinted(...)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 selectedandextra vault existscases, not only happy-pathone vaultdeployments. A topology mistake that changes which object definestotalDepositedis 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
principalfor 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 whosetotalDepositedis 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 - totalDepositedis 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.
Harvestis 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
principalandvaluecome 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 safecan 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 profitwas 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
- Publication-authority pattern: state-publication-authority-failure — the exploit only paid because
harvest()blessed misclassified vault state as canonical debt-repayment truth. - Adjacent control-surface note: sync-harvest-and-cache-write-authority-discipline — the decisive failure was that a
harvest()path was allowed to publish misclassified balance-sheet state as canonical debt-repayment truth. - Adjacent pattern: solvency-check-bypass — the exploit paid because the system’s debt ledger accepted false repayment state and then honored withdrawals against it.
- Adjacent pattern: reward-debt-desynchronization-denominator-collapse-reward-amplification — not the same mechanic, but a nearby reminder that reward / yield distribution logic can become an issuance or debt-erasure primitive when global and per-module accounting drift apart.
- Adjacent design note: reward-recognition-presentation-and-collateral-separation — the broader design lesson is that internal reward recognition should not become immediate solvency truth until the underlying accounting object is unquestionably canonical.
Control surface
-
The decisive control surface was deployment-time vault topology plus the later
harvest(vaultId)authority to crystallizeadapter balance - recorded depositsinto real system-wide debt reduction. -
Alchemix’s alETH stack did not fail because an external attacker forged state; it failed because internal module state stopped agreeing on which vault object represented real principal.
-
The sharp lesson is that array position, active-vault selection, and rollout scripts are part of protocol solvency design whenever
profit = totalValue - recordedPrincipaldrives debt forgiveness. -
Sources:
- https://rekt.news/alchemix-rekt
- https://slowmist.medium.com/slowmist-alchemix-hack-analysis-e8c9ec6c2ee3
- https://x.com/AlchemixFi/status/1405187348678148101
- https://alchemixfi.medium.com/behold-aleth-90d750889fd8
- https://forum.alchemix.fi/public/d/137-incident-report-06162021
- https://raw.githubusercontent.com/alchemix-finance/alchemix-protocol/master/contracts/Alchemist.sol
- https://raw.githubusercontent.com/alchemix-finance/alchemix-protocol/master/contracts/Transmuter.sol
- https://raw.githubusercontent.com/alchemix-finance/alchemix-protocol/master/contracts/libraries/alchemist/Vault.sol
-
Last reviewed: 2026-08-03 UTC