Akropolis SavingsModule Malicious-Token Reentrancy / PoolToken Over-Mint (2020-11-12)
- Name: Akropolis SavingsModule malicious-token reentrancy / poolToken over-mint
- Date: 2020-11-12
- Protocol: Akropolis Delphi Savings Pools
- Chain: Ethereum
- Category: balance-delta provenance failure / savings-pool share inflation / malicious-token admission / reentrancy / unbacked poolToken mint
- Loss: about 2,030,841.0177 DAI from the yCurve and sUSD pools
- Summary: Akropolis let
SavingsModule.deposit()accept an attacker-supplied token address and mint pool tokens from a before/after balance delta measured arounddepositToProtocol(). The attacker supplied a malicious ERC20-like token whosetransferFrom()re-entereddeposit()with real DAI borrowed from dYdX. The real DAI only entered once, inside the nested inner call. But when that inner deposit finished and control returned to the outer fake-token frame, the outer frame re-measured the same DAI balance increase as if it were its own deposit. Akropolis therefore counted one economically real DAI inflow twice, minted too many pool tokens, and then let those unbacked pool tokens redeem against real stablecoin liquidity in the yCurve and sUSD pools. - Trigger / entry point:
SavingsModule.deposit()trusted the caller-supplied token and did not enforce that the token belonged to the supported asset set for the pool.- The deposit path lacked reentrancy protection around the balance-delta accounting that determined poolToken mint quantity.
- The attacker used malicious token contract
0xe2307837524db8961c4541f943598654240bd62f, whosetransferFrom()executed a hook that re-entered Akropolis.
- Root cause:
- Akropolis treated a token transfer as if it were a passive accounting step rather than a full control-flow surrender.
- PoolToken minting depended on the pool balance difference before and after
depositToProtocol(), but that balance-delta method was not safe under reentrancy. - Because the attacker could re-enter
deposit()with real DAI during the fake-tokentransferFrom(), the one economically real DAI deposit landed in the inner frame while the outer fake-token frame later claimed that same balance increase as its own and minted a second, unbacked set of pool tokens. - The exploit also depended on unsupported-token admission: the protocol let an attacker choose a crafted token contract for a path that should only have interacted with known assets.
- Exploit path:
- The attacker borrowed capital with a dYdX flash loan and targeted Akropolis’s stablecoin savings pools.
- The attacker called
SavingsModule.deposit()using malicious token contract0xe2307837524db8961c4541f943598654240bd62fas the token argument. - During the outer call’s
transferFrom()on that fake token, the malicious token re-enteredSavingsModule.deposit(). - The re-entrant inner deposit used real DAI — public reporting cites roughly 800,000 DAI in the main setup — and legitimately increased the pool’s DAI balance.
- The inner call completed and minted poolTokens once against that real DAI balance increase.
- Execution then returned to the outer fake-token deposit, which resumed its own before/after balance check.
- Because the pool balance had risen during the inner DAI deposit, the outer call observed the same increase and minted a second set of poolTokens even though the fake token had contributed no real value.
- The attacker redeemed the over-minted poolTokens out of the yCurve and sUSD pools, repeating the pattern across 17 exploit transactions until about 2.03 million DAI had been drained.
- What the attacker needed to be true:
- A pool mint path had to accept an attacker-chosen token contract instead of only an allowlisted supported asset.
- Re-entrancy into
deposit()had to remain possible during the token transfer path. - PoolToken issuance had to depend on a balance delta that could be observed both by the inner and outer deposits.
- The over-minted poolTokens had to remain redeemable for real assets in downstream pool accounting.
- Sufficient stablecoin liquidity had to remain in the affected pools for the fake poolTokens to cash out.
- Technical anatomy worth remembering:
- This was not just
withdraw before balance update. It was a share-mint double count: one real DAI deposit was priced twice because a malicious token controlled the timing of the balance-delta measurement. - The mechanically important detail is where the real value entered: not in the outer fake-token call, but at the bottom of the two-frame nested stack when the inner DAI deposit completed. The outer frame then resumed and claimed that already-booked DAI increase a second time.
- The unsupported-token angle mattered as much as the missing reentrancy guard. Akropolis let a hostile token contract participate in a mint path that assumed vanilla token semantics.
- PeckShield’s example makes the invariant failure vivid: if the inner deposit moved 25,000 DAI, both the inner and outer calls could mint as if they had each delivered that 25,000 DAI, turning one real inflow into 50,000 DAI worth of poolTokens.
- That makes Akropolis a clean two-frame cousin to Grim’s later multi-frame vault-share version: the accounting bug is not merely
reentrancy happened, buteconomically real value entered once in the nested frame and earlier bookkeeping frames still monetized it on the way back out. - The profits were realized only because Akropolis honored the resulting poolTokens as real redemption claims against live yCurve and sUSD liquidity.
- This was not just
Three receipt-authority promotions this incident collapsed
- Some balance movement existed at the pool boundary
- A real DAI inflow really did land in Akropolis during the nested inner deposit.
- But that only proved
the pool balance changed somewhere during this stack, notthis outer deposit frame earned the change.
- That observed movement still counted as this exact deposit frame’s receipt truth
- This is the promotion Akropolis got wrong. The inner DAI deposit had already consumed the real receipt event and minted poolTokens once.
- When control returned to the outer fake-token frame, Akropolis re-read the same ambient balance increase and promoted it into fresh receipt truth for a second deposit story.
- The crucial failure was not merely reentrancy in the abstract, but that the protocol could not keep
one real inflowbound toone exact deposit frame.
- That accepted receipt truth still deserved live mint-and-redemption authority
- Even after the protocol observed a balance increase, healthy design would still need one bounded claim path, delayed redemption, or some other brake before ambiguous receipt state became cash-equivalent poolTokens.
- Akropolis instead let the second receipt story inherit ordinary poolToken mint authority immediately, and those extra poolTokens were then redeemable against live yCurve and sUSD liquidity.
This is why Akropolis is such a useful corpus case: the callback window mattered, but the monetizable jump was from some real DAI entered during nested execution to two different deposit frames may both publish that same DAI as their own backing truth.
- Representative transactions and addresses:
- Vulnerable Akropolis contract highlighted in public analysis:
0x73fC3038B4cD8FfD07482b92a52Ea806505e5748 - Malicious token / attack contract:
0xe2307837524db8961c4541f943598654240bd62f - dYdX flash-loan kickoff transaction cited by PeckShield:
0xddf8c15880a20efa0f3964207d345ff71fbb9400032b5d33b9346876bd131dc2 - Representative exploit transaction:
0xe1f375a47172b5612d96496a4599247049f07c9a7d518929fbe296b0c281e04d - PeckShield-tracked stolen-funds wallet:
0x9f26ae5cd245bfeeb5926d61497550f79d9c6c1c
- Vulnerable Akropolis contract highlighted in public analysis:
- Response / mitigation:
- Akropolis paused its stablecoin pools after detecting the drain and said other pools, including bUSD, sBTC, Aave, and Compound integrations, were unaffected.
- The team said the exploit combined reentrancy with flash-loan-funded execution and began fixing contract-level issues.
- Akropolis later offered the attacker a $200,000 bug bounty in exchange for returning funds and said it was adding token checks / whitelist functions, more test coverage, and broader security review.
Pattern tags
unsupported token admission let attacker-authored transfer semantics decide when share-mint accounting reopenedone real DAI inflow at the bottom of the stack was minted once in the inner frame and again in the outer framepoolToken redemption gave a deposit-accounting mistake direct line-of-sight to live yCurve and sUSD liquidityearly seam case where interface-conformance failure opened the door and balance-delta provenance failure paid out
Why the exploit worked
- Akropolis did not merely miss a
nonReentrantmodifier. It let a caller-supplied token contract participate in a mint path that assumed honest supported-asset semantics, then priced poolToken issuance from a raw before/after balance delta around that external call. - The malicious token’s economic job was effectively zero; its real job was to reopen the deposit frame. The only economically meaningful inflow was the nested real DAI deposit, but the outer frame later resumed and treated that already-booked DAI increase as if it were its own receipt event.
- That made the core invariant failure
one live stablecoin inflow, two redeemable poolToken stories. Once Akropolis honored those extra poolTokens as ordinary claims, the accounting bug immediately became withdrawable yCurve and sUSD liquidity rather than a paper-only share inflation. - This is why Akropolis matters as more than generic reentrancy: the callback window was the transport, but the monetizable mistake was provenance. The protocol never proved which exact deposit frame had earned the observed balance increase.
Detection and audit pivots
- Treat any
deposit(token, ...)or router-style mint path as high risk when the caller chooses the token contract and the protocol later mints frombalanceAfter - balanceBefore. Supported-asset validation and accounting-boundary discipline have to be reviewed together. - Fuzz malicious
transferFrom()callbacks that re-enter the same deposit path with the real asset on the inner frame. The key test is not onlycan reentrancy happen, butcan one real inflow be observed by two accounting frames. - Distinguish
the contract balance increasedfromthis exact call delivered the increase. If the code cannot prove source-specific receipt, it is already one step away from double-crediting the same backing. - Review whether any over-minted receipt token is immediately redeemable against live pool assets. Share inflation is especially dangerous when the mis-minted object already has a routine cash-out path into reserve liquidity.
- Ask whether unsupported-token admission is being treated as a UX detail instead of a solvency boundary. In Akropolis, the hostile token contract was not an incidental parameter mistake; it was the mechanism that rewrote when deposit truth was allowed to be observed.
Why this incident matters
- It is a clean seam case between unsupported-token admission and balance-delta provenance failure: the hostile token is what opens the path, but the monetizable invariant break is that one real DAI inflow gets credited to two accounting frames.
- It is also a clean early vault/share-accounting cousin to Grim’s later multi-loop vault exploit: Akropolis preserves the simplest two-frame form of the same broader lesson that the last real deposit at the bottom of a nested call stack can be re-measured and re-minted by earlier frames on unwind.
- The case broadens the reentrancy corpus beyond ERC-777 collateral overwrites and plain recursive withdrawals: here the attacker used callback control to make one real deposit count twice.
- It is also a reminder that delta-based mint accounting is only safe if the delta cannot be adversarially altered mid-measurement.
Failure-prevention checklist
- Constrain deposit assets at the path boundary. If a savings pool is meant to accept only specific stablecoins or known wrappers, enforce that allowlist before any transfer or mint logic begins rather than trusting a caller-supplied token argument.
- Separate
actual receiptfromwho earned credit for that receipt. Even correctbalanceAfter - balanceBeforemeasurement is not enough unless one exact deposit frame, pool, and recipient is bound to that observed increase. - Close the whole share-mint accounting window before any attacker-influenced external call can move the same asset. If token transfer must happen first, the later mint step still needs a source-specific receipt object rather than a fresh read of ambient pool balance.
- Treat callback-capable or semantically exotic tokens as unsupported by default unless an adapter narrows them into the exact semantics the deposit path expects.
- Keep accounted reserves separate from raw token inventory so a real inflow recognized by one frame cannot be silently rediscovered and credited by another frame on unwind.
- Make over-minted receipt tokens hard to cash out while an accounting anomaly is still plausible: supply caps, withdrawal throttles, or delayed redemption do not fix the bug, but they reduce how quickly double-credited shares become drained stablecoin liquidity.
- Fuzz nested
deposit(fake token) -> transferFrom callback -> deposit(real token) -> unwind outer frametraces directly. The invariant to assert isone economically real inflow can mint at most one redeemable poolToken claim. - Audit all helper and wrapper paths that can end in the same mint logic. A semantic boundary enforced only on the happy-path deposit entrypoint is not a real boundary.
Audit prompts
- Which exact assets is this deposit path supposed to support, and where in code is that support contract enforced rather than merely documented?
- What exact economic fact does the measured balance delta prove here:
tokens reached the contract, orthis specific deposit call delivered those tokens and therefore earned new poolTokens? - Between the first balance snapshot and the final poolToken mint, can any token hook, callback, sibling entrypoint, or nested call move the same underlying asset?
- If a real stablecoin deposit lands during a nested frame, what prevents an earlier outer frame from later resuming and claiming that same inflow as its own receipt event?
- Does any later sync, invest, or reserve-recognition helper reinterpret ambient token balances independently of the mint path that already consumed them?
- If an unsupported token address is passed in, does the path fail closed immediately, or can interface-compatible but semantically hostile behavior still reach accounting logic?
- Once poolTokens are minted, what controls prevent immediate redemption against live stablecoin liquidity if the mint was unbacked or ambiguously backed?
- Would a new reviewer looking only at tests learn that one real inflow must never satisfy two nested deposit frames, or is that invariant still mostly tribal knowledge?
Lessons that should already be standard
Caller chooses the tokenis already a solvency boundary, not just an integration detail. If the token can define transfer semantics, it can often define accounting timing too.- Reentrancy is only half the story. The profitable mistake here was provenance: the protocol could not prove which frame owned the observed DAI increase.
balanceAfter - balanceBeforeis evidence of movement, not authorship. A measured balance increase can still belong to the wrong user, wrong frame, or wrong economic event.- Share minting and redemption cannot be analyzed separately. If a dubious receipt path can mint an ordinary claim token, the cash-out path has already become part of the exploit surface.
- Interface-compatible is not accounting-compatible. A token that satisfies the expected ABI can still be disallowed because its transfer behavior is too powerful for the path’s assumptions.
- One live inflow should produce one redeemable claim story. If the system can tell two coherent narratives about who earned the same backing, the attacker will cash out both.
Internal linkages
- Core recurring pattern: reentrancy.
- Asset-admission lens: interface-conformance-semantic-compatibility-failure — Akropolis first failed by letting an attacker-chosen fake token into a deposit path that assumed a supported asset with passive transfer semantics.
- Receipt-boundary companion: actual-receipt-accounting-and-token-semantics-admission-discipline — the deposit path needed a much narrower semantic contract than
caller supplied something ERC20-like and balances changed, because the entire exploit rode on hostile transfer semantics being admitted into share-mint accounting. - Accounting lens: balance-delta-provenance-failure — Akropolis is the clean two-frame version of that family: one economically real DAI deposit landed in the inner frame, and one earlier outer frame resumed and claimed that same balance increase again on unwind.
- Callback-frame companion: callback-frame-truth-security-object — the practical control question is not only
can deposit re-enter, butwhich unfinished deposit frame is still allowed to claim the next observed inflow as its own.
Control surface
-
The leverage sat in token admission policy, reentrancy boundaries around
deposit(), and the protocol assumption that a balance delta measured around an external token transfer still represented one economically real deposit. -
Akropolis did not just miss a lock. It let a hostile token redefine what counted as
the depositinside its own share-mint math. -
Sources:
- https://peckshield.medium.com/akropolis-incident-root-cause-analysis-c11ee59e05d4
- https://decrypt.co/48081/defi-platform-akropolis-admits-to-being-hacked-for-2-million
- https://www.coindesk.com/tech/2020/11/12/defi-project-akropolis-drained-of-2m-in-dai
- https://www.zdnet.com/article/cryptocurrency-exchange-dangles-bug-bounty-carrot-to-hacker-who-stole-2-million/
- https://coingeek.com/2-million-stolen-in-akropolis-defi-exploit/
-
Last reviewed: 2026-08-03 UTC