Zunami Protocol UZD / zETH SDT-Donation Price-Caching Share-Inflation Drain (2023-08-13)

  • Name: Zunami Protocol UZD / zETH SDT-donation price-caching share-inflation drain
  • Date: 2023-08-13
  • Protocol: Zunami Protocol
  • Chain: Ethereum
  • Category: donation / share-price manipulation / oracle manipulation / cached-price abuse / strategy-accounting inflation
  • Loss: about 1,204 ETH total, with public breakdowns of roughly 26 WETH from the earlier zETH hit and about 1,178 ETH from the main UZD exploit, or about $2.1M-$2.2M at the time
  • Summary: Zunami’s main stablecoin exploit was not a simple AMM spot push by itself. The attacker first bought large amounts of UZD, then donated SDT directly into Zunami’s MIMCurveStakeDao strategy and simultaneously pumped the SushiSwap markets that Zunami used to value that SDT. Because Zunami’s totalHoldings() counted both the strategy’s raw SDT balance and an attacker-moveable sdtPrice, the strategy suddenly looked much richer than it really was. The attacker then called cacheAssetPrice() on the UZD contract, freezing that inflated asset price for the rest of the block. Since UZD balanceOf() and related logic multiplied user share balances by assetPriceCached(), the attacker’s apparent UZD balance jumped from about 4.87M to about 16.90M without any legitimate yield having been earned. They then unwound the SDT price manipulation, dumped the inflated UZD into Curve’s UZD pools, repaid the flash loans, and kept roughly 1,152 WETH profit in the main transaction after an earlier smaller zETH drain the same day.
  • Trigger / entry point:
    • Zunami’s elastic stablecoins exposed a public cacheAssetPrice() path that wrote a block-cached strategy value into _assetPriceCached.
    • UZD version 1.1 extended that cached price into user-facing functions such as balanceOf, totalSupply, and allowance, so one inflated cache write could affect token accounting broadly for the rest of the block.
    • One underlying strategy, MIMCurveStakeDao, treated totalHoldings() as _lpBalance * lpPrice + sdt * sdtPrice + config.crv * crvPrice, so direct SDT donations and an attacker-shaped sdtPrice both increased reported holdings.
    • The sdtPrice path depended on thin SushiSwap pairs that a flash-loan attacker could move briefly.
  • Root cause:
    • Zunami treated strategy-marked holdings as if they were manipulation-resistant net asset value.
    • The protocol let one actor increase the strategy’s accounted SDT inventory by direct donation, then let the same actor mark that donated inventory at an attacker-controlled spot price.
    • It then cached that manipulated price and reused it in token-balance math rather than only in a delayed or sanity-checked accounting path.
    • In practice, Zunami fused two brittle assumptions into one exploit surface: that strategy-side balances only change through trusted yield paths, and that a same-block cached asset price is safe to treat as token balance truth.
  • Exploit path:
    1. The attacker used EOA 0x5f4c21c9bb73c8b4a296cc256c0cde324db146df and attack contract 0xa21a2b59d80dc42d332f778cbb9ea127100e5d75.
    2. In the main UZD transaction 0x0788ba222970c7c68a738b0e08fb197e669e61f9b226ceec4cab9b85abe8cceb, the attacker flash-loaned about 7,000,000 USDT from Uniswap v3 plus about 7,000,000 USDC and 10,011 WETH from Balancer.
    3. They used those funds to buy into Zunami’s Curve pools, acquiring about 4,082,036 UZD from a crvFRAX -> UZD route and about 791,280 UZD from a crvUSD -> UZD route, for about 4.87M UZD total.
    4. They swapped about 11 WETH for roughly 55,981 SDT and transferred that SDT directly into the vulnerable MIMCurveStakeDao strategy, increasing the strategy’s raw SDT balance without minting corresponding UZD shares.
    5. They then performed the price-printing leg on SushiSwap, swapping about 10,000 WETH for about 58,042 SDT in the SDT/WETH pair and about 7,000,000 USDT for about 2,154 WETH in the WETH/USDT pair, which together inflated the strategy’s derived sdtPrice.
    6. With both sdt balance and sdtPrice distorted, they called cacheAssetPrice() on UZD contract 0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c, freezing the manipulated price into _assetPriceCached for the rest of the block.
    7. Because UZD balance math used balance[address] * assetPriceCached() / DEFAULT_DECIMALS_FACTOR, the attacker’s apparent UZD balance jumped from about 4,873,316 UZD to about 16,902,957 UZD.
    8. The attacker then reversed the SDT and WETH price-push trades, restoring the manipulated markets while keeping the inflated cached UZD accounting alive long enough to matter.
    9. They sold about 14,158,424 UZD back into one Curve pool for about 7,281,124 crvFRAX and about 2,744,533 UZD into another for about 1,891,351 crvUSD, then exited into hard assets, repaid the flash loans, and kept about 1,152 WETH from the main path.
    10. Public postmortems also note an earlier same-day zETH exploit path that drained about 26 WETH before the larger UZD transaction.
  • What the attacker needed to be true:
    • cacheAssetPrice() had to be publicly callable during the manipulated state window.
    • balanceOf() and related token logic had to trust the cached asset price quickly enough that one-block inflation could be monetized.
    • The MIMCurveStakeDao strategy had to count direct SDT balances in totalHoldings() rather than tracking only internally accounted deposits.
    • priceTokenByExchange() for SDT had to rely on thin SushiSwap pairs the attacker could overwhelm with flash-loaned capital.
    • Curve pools for UZD and zETH needed enough liquidity that the attacker could dump the inflated shares before operators paused the system.
  • Technical anatomy worth remembering:
    • This was not just oracle manipulation in the narrow sense of reading a bad AMM price. The decisive failure was that Zunami turned a manipulated strategy valuation into token-balance truth through cached share-price accounting.
    • It also was not a pure ERC-4626-style donation attack. The attacker did not merely donate assets and borrow against a higher exchange rate elsewhere; they donated SDT into a strategy, pumped the strategy’s pricing inputs, cached the resulting NAV, and made their wallet’s UZD balance itself expand.
    • The same-block cache mattered enormously. Without cacheAssetPrice(), the attacker would have needed the manipulated strategy value to remain live through all later swaps. By snapshotting it, they could reverse the price-push legs while preserving the inflated UZD accounting long enough to cash out.
    • Ackee’s postmortem is also worth preserving for process reasons: the vulnerable global caching extension shipped in UZD v1.1 without a fresh audit, and the attacked strategy had been added after an earlier audit of a different protocol version.
  • Representative transactions, contracts, and markets:
    • Attacker EOA: 0x5f4c21c9bb73c8b4a296cc256c0cde324db146df
    • Attack contract: 0xa21a2b59d80dc42d332f778cbb9ea127100e5d75
    • Main UZD exploit tx: 0x0788ba222970c7c68a738b0e08fb197e669e61f9b226ceec4cab9b85abe8cceb
    • Earlier zETH exploit tx: publicly cited in REKT as 0x2aec4fdb...
    • UZD contract with cacheAssetPrice(): 0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c
    • totalHoldings() strategy contract called out by CertiK: 0x2ffcc661011bec72e1a9524e12060983e74d14ce
    • SushiSwap SDT/WETH pair used in price calculation: 0x22def8cf4e481417cb014d9dc64975ba12e3a184
    • SushiSwap WETH/USDT pair used in price calculation: 0x06da0fd433c1a5d7a4faa01111c044910a184553
  • Response / mitigation:
    • Zunami confirmed the attack shortly after external alerts and stated that base collateral remained secure while the team investigated.
    • Public analysis later converged on the cached-price path plus strategy-accounting design as the real exploit surface, not Curve pool logic itself.
    • The incident became a useful cautionary example that performance-oriented caching and newly added strategies both belong inside the protocol’s live security boundary and need fresh review when they start affecting solvency or token-balance math.

Why this incident matters

  • Zunami broadens the donation / share-price corpus with a mechanically distinct cousin: the attacker did not mainly print fake collateral to borrow elsewhere, but to make their own elastic-balance stablecoin holdings expand inside the token contract and then dump those inflated claims into live liquidity pools.
  • More specifically, the exploit path was one joined authority object: strategy-side totalHoldings() counted attacker-donated SDT at an attacker-shaped sdtPrice, public cacheAssetPrice() froze that manipulated NAV into _assetPriceCached, and UZD v1.1 then fanned that cached publication out through balanceOf(), totalSupply(), and allowance, turning one public cache write into wallet-balance truth for the rest of the block.
  • It therefore preserves a sharp bridge between donation / share-price manipulation, oracle manipulation, and state publication authority failure: strategy-side donation made the accounting lie possible, thin-market AMM manipulation made the donated inventory look expensive, and block-level caching made the transient lie spendable.
  • The case is a strong reminder that cached NAV is still an oracle, and if that NAV can be moved by attacker-shaped balance changes or thin AMM marks, caching it can make the exploit easier rather than harder.

Three cache-and-balance-authority promotions this incident collapsed

  1. Attacker-shaped strategy value existed inside the MIMCurveStakeDao path

    • Donated SDT sat in the strategy contract and the attacker simultaneously pushed the SushiSwap legs that priceTokenByExchange() used to derive sdtPrice.
    • At this stage the honest statement should still have been narrow: the strategy currently appears to hold more SDT-valued inventory under a manipulable spot mark. That is not yet the same claim as UZD holders have earned more value or wallet balances deserve to expand.
  2. Zunami published that strategy distortion as canonical token-wide asset-price truth

    • Public cacheAssetPrice() promoted the attacker-shaped strategy valuation into _assetPriceCached at the exact peak of the manipulation window.
    • This was the decisive publication jump. A thin-market, donation-sensitive strategy estimate stopped being a provisional internal view and became the canonical asset-price object that UZD v1.1 told the rest of the system to trust for the block.
  3. Token semantics and live pool exits spent against that published truth

    • UZD balanceOf(), totalSupply(), and related accounting surfaces reused the cached mark, so the attacker’s wallet appeared to hold far more UZD before any real yield had been earned.
    • Curve pools then honored that inflated token-side truth when the attacker dumped the expanded UZD balance back into live liquidity for harder assets.

The recurring review mistake is to collapse those three promotions into one vague sentence like the asset price was manipulated. Zunami only became a real loss event because attacker-shaped strategy inventory first became canonical cached balance truth and then inherited immediate pool-exit authority.

Pattern tags:

  • Strategy-balance donation recognition: MIMCurveStakeDao.totalHoldings() counted raw SDT sitting in the strategy contract, so direct third-party transfers into the strategy became numerator-side value without minting proportional UZD exposure.
  • Thin-market multi-hop price printing: priceTokenByExchange() depended on SushiSwap SDT/WETH plus WETH/USDT, so one flash-loan actor could reshape the sdtPrice path cheaply enough inside a single transaction.
  • Public cache-write publication failure: cacheAssetPrice() let any caller promote that attacker-shaped strategy valuation into _assetPriceCached at the exact peak of the manipulation window.
  • Token-balance fan-out from cached NAV: UZD v1.1 reused the cached asset price in balanceOf(), totalSupply(), and allowance, so one bad publication did not stay a misleading view; it became wallet-balance truth across multiple token-facing surfaces.
  • Design companions: state-publication-authority-failure, sync-harvest-and-cache-write-authority-discipline, internal-mark-publication-quarantine-and-reserve-spend-separation, and delayed-snapshots-and-same-block-quarantine.

Why the exploit worked

  • The attacker first acquired a meaningful UZD position, so the later cache write revalued a large existing balance they already controlled rather than only a dust holding.
  • MIMCurveStakeDao.totalHoldings() collapsed two attacker-shaped inputs into one trusted strategy value: raw donated SDT balance and a spot-derived sdtPrice that could be pushed through thin SushiSwap liquidity.
  • Zunami exposed the decisive publication step publicly. The same actor who distorted the strategy valuation could immediately call cacheAssetPrice() before anyone else observed or challenged the mark.
  • UZD v1.1 then treated the cached mark as token-accounting truth, so the manipulated NAV propagated into balanceOf() and related user-facing semantics instead of remaining a provisional internal estimate.
  • Because the cache lasted for the block, the attacker could unwind the visible SDT/WETH price manipulation and still keep the inflated UZD balance live long enough to dump it into Curve liquidity and exit in harder assets.

Detection and audit pivots

  • Review every strategy totalHoldings() / NAV path that mixes raw token balances with live AMM-derived marks. Ask who can donate into those balances, who can move the pricing venue, and whether both rails can be combined in one transaction.
  • Treat public cache, sync, harvest, and valuation-publication helpers as security-sensitive authority surfaces whenever downstream token math, collateral math, or redemption math consumes their outputs.
  • Separate informational marks from spendable marks. If balanceOf(), totalSupply(), allowance, redemption math, or collateral admission can jump immediately after one cache write, the protocol has collapsed publication and settlement authority.
  • Fuzz the exact sequence acquire shares -> donate into strategy -> move thin pricing venue -> cache mark -> unwind venue -> dump inflated claims, not just isolated oracle-manipulation or donation steps.
  • Monitor for same-block asset-price or NAV jumps that are not matched by explicit deposits, realized yield, or trusted operator actions, especially when the jump coincides with large venue slippage and immediate redemption / swap activity.

Failure-prevention checklist

  • Make strategy NAV depend on accounted positions, not ambient token balances. If MIMCurveStakeDao.totalHoldings() wants to recognize SDT, it should do so through explicit strategy-owned accounting or a sweep path that cannot be credited by arbitrary third-party transfers.
  • Treat thin-governance-token marks as non-canonical for reserve accounting. If a strategy still holds tokens like SDT, price them through delayed or liquidity-aware oracles, haircut them aggressively, or quarantine them from immediate share-price publication entirely.
  • Keep cache-write authority away from spendable token semantics. A helper like cacheAssetPrice() should never let one caller promote a same-block manipulated mark into balanceOf(), totalSupply(), allowance, redemption, or collateral truth without delay and independent validation.
  • Separate publication from settlement by at least one state transition the attacker cannot fully script. If a strategy mark changes sharply, wallet-balance semantics and reserve-spending paths should warm up more slowly than the informational view.
  • Re-audit whenever a pricing cache fans out into new user-facing surfaces. Ackee’s postmortem is the clean governance lesson here: UZD v1.1 extended caching into balanceOf() / allowance() and the attacked MIMCurveStakeDao strategy was added after an earlier audit, so the live solvency object had changed even if the protocol branding had not.
  • Maintain strategy-level kill switches and per-token quarantine knobs. If one strategy suddenly becomes dominated by donated or thinly priced inventory, operators should be able to exclude it from aggregate NAV or freeze cache publication before pool exits turn the mark into hard-asset losses.
  • Alert on strategy balance jump + thin-market price jump + cache write + immediate large redemption as one compound incident signature rather than four unrelated monitors.

Audit prompts

  • Which exact balances inside MIMCurveStakeDao.totalHoldings() can be increased by arbitrary third-party transfers, and which of those balances are later multiplied by manipulable spot prices?
  • What venue composition produced sdtPrice, how much real depth backed those SushiSwap pairs, and could one flash-loan actor move both legs of the price path in the same transaction?
  • Can any caller still write a cached mark that immediately changes balanceOf(), totalSupply(), allowance, redemption ratios, or other spend-authoritative surfaces?
  • If a wallet first acquires a large UZD position, what exact controls stop that same wallet from donating into a strategy, printing the strategy mark, caching it, unwinding the visible price push, and then dumping the inflated claims before the cache expires?
  • What changed after the last audit: new strategies, new pricing venues, new cache fan-out into token semantics, or new helper functions that turned an internal estimate into a public authority surface?
  • Does the protocol distinguish view-layer convenience cache from canonical solvency mark, or are those still the same storage slot and the same trust object?
  • If a strategy-reported NAV jumps sharply without matching deposits or harvested yield, what path prevents the protocol from instantly honoring that jump in user balances or exits?

Lessons that should already be standard

  • Caching a manipulable mark does not neutralize it; sometimes it does the opposite. Zunami made the attack practical by preserving a one-block lie long enough for the attacker to unwind the visible price push and still cash out the frozen fiction.
  • Strategy inventory is not automatically earned value. If arbitrary users can donate a token into a strategy, raw balance growth is an input-authentication problem before it is a pricing problem.
  • balanceOf() is solvency-critical when it derives from cached NAV. Once wallet-balance semantics inherit a strategy mark, a bad publication is no longer a misleading dashboard value; it is a spendable claim.
  • Public publication helpers deserve the same skepticism as admin or oracle updaters when downstream exits trust them. Anyone can call it is not harmless if anyone can call it at the peak of a manipulation window.
  • Audit scope drift compounds quietly. Zunami’s vulnerable UZD v1.1 cache fan-out and the attacked MIMCurveStakeDao strategy each changed the live risk object after earlier review, which is exactly how a protocol can keep the same brand while shipping a different solvency boundary.
  • Donation-sensitive accounting and thin-market pricing become much worse together than apart. The dangerous object was not just donated SDT or just bad Sushi price; it was their multiplication, publication, and immediate redemptionability inside one transaction.

Internal linkages

  • Core recurring pattern: donation-share-price-manipulation.
  • Adjacent pattern: oracle-manipulation — the attacker still had to print a manipulable price through thin Sushi markets, but the monetization path ran through cached share-price accounting rather than a simple collateral borrow.
  • Publication-authority pattern: state-publication-authority-failure — the transient strategy-side lie only became spendable because cacheAssetPrice() froze it as canonical token-balance truth for the rest of the block.
  • Publication-authority companion: sync-harvest-and-cache-write-authority-discipline — cacheAssetPrice() mattered because one public cache write was allowed to promote attacker-shaped strategy NAV into live token-balance truth immediately.
  • Publication-quarantine companion: internal-mark-publication-quarantine-and-reserve-spend-separation — the manipulated strategy mark should not have graduated into immediate wallet-balance and redemption authority in the same block it was published.
  • Timing companion: delayed-snapshots-and-same-block-quarantine — the exploit specifically depended on freezing a one-block distortion long enough to unwind the visible price-push legs while keeping the inflated UZD balance spendable.
  • Presentation / collateral timing companion: reward-recognition-presentation-and-collateral-separation — Zunami collapsed strategy-side recognition, cached presentation, and spendable token-balance semantics into one same-block path instead of graduating them at different speeds.
  • Upstream accounting companion: vault-share-truth-security-object — the healthier boundary was one coherent share / asset object where donated strategy inventory, cached NAV publication, and user-visible balance semantics could not drift into separate stories.
  • Adjacent pattern: oracle-source-independence-failure — not the main failure here, but a useful reminder that multi-hop onchain price composition can still collapse onto one thin venue family.

Control surface