Wise Lending Stealth Donation / Share-Price Manipulation (2024-01-12)

  • Name: Wise Lending stealth donation / share-price manipulation
  • Date: 2024-01-12
  • Protocol: Wise Lending
  • Chain: Ethereum
  • Category: lending-vault exchange-rate manipulation / stealth donation / collateral inflation / bad-debt extraction
  • Loss: about 170 ETH, commonly reported as roughly 466K
  • Summary: The attacker targeted Wise Lending’s Pendle LP vault path, first creating an extreme assets-per-share ratio with a dust opening deposit plus a direct donation deliberately kept just below Wise’s donation throttle. Public technical analysis says that opening donation was roughly 5,000,000x the size of the initial deposit. The attacker then repeatedly amplified the distorted ratio through rounding-driven stealth donations: each loop sacrificed a little value on deposit and withdraw, but those rounding leftovers stayed inside the vault without minting matching shares, so assets per share compounded upward. Once each vault share was absurdly overpriced on paper, the attacker split the position across puppet accounts, borrowed real assets from other Wise pools against the fake collateral value, then collapsed the manipulated vault structure so the protocol was left short while the attacker kept the borrowed assets.
  • Trigger / entry point:
    • Wise used vault share value in a lending context where inflated shares could unlock borrowing power in other pools.
    • The attacked pool was the Wise PLP pool for Pendle LP tokens.
    • Wise had anti-donation measures — internal accounting plus a donation throttle — but the relevant code path still recognized some unsolicited balance growth as real pool value. On each syncPool() call, _preparePool() invoked _cleanUp(), which compared the contract’s live token balance to totalPool + totalBareToken and credited the pool with the difference up to allowedDifference rather than discarding it entirely.
    • That meant a direct donation kept just below the throttle was still enough to reprice the pool when totalDepositShares was tiny.
    • Public technical writeups pointed to the share-conversion helper calculateLendingShares() / _calculateShares() as the second key ingredient: deposit and withdraw-by-amount paths intentionally rounded in the vault’s favor, so repeated loops could leave extra assets behind without minting matching shares.
  • Root cause:
    • Wise treated the vault as if direct-donation throttles and internal accounting were enough, but _cleanUp() still admitted below-throttle unexpected balance growth into pseudoTotalPool and totalPool, so some attacker-authored donations were still promoted into canonical share-price state.
    • Deposit and withdrawal math then amplified that state. In the audited code, calculateLendingShares() called _calculateShares(product, pseudo, false) for deposits, which returned product / pseudo - 1, while amount-based withdrawals called _preparationsWithdraw() and then _calculateShares(product, pseudo, true), which returned product / pseudo + 1.
    • In ordinary operation that vault-favoring bias looks conservative; in Wise’s low-share state it meant the attacker could repeatedly transfer value into the vault without minting or preserving proportional shares, i.e. manufacture stealth donations on demand.
    • The vault did not discard or neutralize those stealth donations when computing exchange rate, so each loop made the next loop stronger.
    • The design also lacked a meaningful dead-share or virtual-liquidity buffer, which let one actor dominate ownership of the manipulated pool and later redeem nearly the whole structure.
    • Wise then let that manipulated vault share price feed directly into borrow power elsewhere in the protocol.
    • In practice, Wise let assets per share become something an attacker could print rather than something the system earned.
  • Exploit path:
    1. The attacker used attack contract 0x91c49cc7fbfe8f70aceeb075952cd64817f9d82c and entered the Wise PLP pool with a tiny initial deposit.
    2. The attacker made a direct donation just below Wise’s throttle limit, creating a very large initial exchange-rate distortion relative to the tiny share base.
    3. The attacker then looped deposits and withdrawals chosen to maximize rounding in the vault’s favor, creating stealth donations that added assets without proportional shares and compounded the exchange rate upward.
    4. After the vault share price was inflated, the attacker deposited enough into the manipulated pool to hold a concentrated share position, while six puppet contracts each obtained a single share.
    5. Each puppet account used its overpriced share as collateral to borrow from other Wise pools, including stablecoin and major-asset pools such as USDC, WETH, USDT, and DAI.
    6. The puppet contracts then withdrew minimal amounts from the manipulated pool, burning their lone shares and leaving the main attacker contract with the remaining dominant claim on the vault.
    7. The main attacker contract redeemed its concentrated share position against the manipulated pool, draining it while the borrowed assets from the sibling pools remained in the attacker’s hands.
  • What the attacker needed to be true:
    • The targeted vault needed to start from a low-share, low-liquidity state where a small initial position could dominate ownership.
    • Wise’s direct-donation defenses had to be bypassable enough that a below-throttle donation could still move the exchange rate materially.
    • Deposit / withdraw rounding had to create stealth donations instead of being discarded from exchange-rate accounting.
    • Other Wise pools had to accept the manipulated vault shares as economically meaningful collateral fast enough to lend out stronger assets.
    • The attacker needed enough operational freedom to spread the position across puppet contracts so borrow-side extraction and final redemption could be staged cleanly.
  • Technical anatomy worth remembering:
    • This was not a simple direct-donation-only exploit. The initial under-throttle donation mattered, but the real force multiplier was iterative stealth donation through rounding.
    • Infect3d’s worked example captures the shape of the bug cleanly: if one share is worth 2 tokens, depositing 3 tokens mints only 1 share after rounding down, leaving 1 token as unrepresented value in the vault; then withdrawing 1 token can still burn 1 whole share after rounding against the user. In a low-supply vault, looping that asymmetry compounds assets-per-share rapidly.
    • In code terms, the same family of helper functions both admitted the initial donation and preserved the later loop: _cleanUp() turned some unexpected balance into recognized pool assets, while calculateLendingShares() / _preparationsWithdraw() biased share conversion against the user on both sides of the round-trip.
    • Wise’s pool-sync invariant also looked in the wrong direction for this exploit. syncPool() ultimately called _compareSharePrices(), which rejected lending share-price decreases and borrow share-price increases; it did not treat sudden attacker-authored lending share-price appreciation itself as suspicious.
    • The vault’s own user-protective rounding conventions became the attacker’s donation engine: assets moved in ways that increased effective vault assets faster than share issuance tracked them.
    • The exploit was cross-module. The vault math bug by itself did not realize profits until Wise’s lending logic treated the printed share price as money-good collateral.
    • Concentrating share ownership was decisive. Public analysis says the main attack contract first positioned itself for 6 shares, then six puppet contracts each minted a single share, borrowed against it, and finally burned that lone share on withdrawal so the main contract ended up holding the remaining meaningful claim on the manipulated pool.
  • Representative transactions and addresses:
    • Attacker EOA identified in public writeups: 0xb90cf1d740b206b6d80854bc525e609dc42b45dc
    • Attack contract: 0x91c49cc7fbfe8f70aceeb075952cd64817f9d82c
    • Vulnerable contract highlighted in public analysis: 0x37e49bf3749513a02fa535f0cbc383796e8107e4
    • Main exploit transaction: 0x04e16a79ff928db2fa88619cdd045cdfc7979a61d836c9c9e585b3d6f6d8bc31
  • Response / mitigation:
    • The incident quickly became a reference case for ERC-4626-style exchange-rate manipulation and stealth donation risk.
    • Post-incident analysis emphasized two missing protections in particular: dead shares or meaningful initial / virtual liquidity, and explicit discarding or neutralizing of stealth donations from rounding.
    • Euler’s later vault-security writeup used Wise as a motivating example for why internal accounting only solves direct donation, not stealth donation, and why share-collateral integrations should be treated as oracle integrations.
    • More broadly, the case reinforced that vault shares used as lending collateral need their own manipulation-aware valuation assumptions, not blind spot exchange-rate trust.

Why this incident matters

  • It is one of the clearest modern incidents showing how donation attack and collateral attack are often the same exploit seen from two different protocol layers.
  • It demonstrates that internal accounting alone is not enough when other vault operations can still create unrepresented assets through rounding.
  • It also makes a broader point that ERC-4626 integrations are oracle integrations in disguise once shares become borrowable collateral.
  • The sharper preserved healthy-side boundary is that Pendle LP share rate is locally readable should not have meant that rate may immediately underwrite full borrow power across healthier sibling pools. Wise needed a stronger downstream distrust boundary than the vault math still says assets per share went up.

Three share-authority promotions this incident collapsed

  1. Unexpected value appeared inside the PLP pool

    • A below-throttle direct donation and then repeated rounding residue left extra Pendle-LP-denominated value inside the pool.
    • At this stage the safest truthful statement should only have been some extra assets are sitting here. That does not yet mean those assets were earned yield, deserved by incumbent shares, or safe to socialize into collateral value.
  2. Wise published that residue as canonical share-price truth

    • _cleanUp() promoted some unsolicited balance growth into totalPool / pseudoTotalPool, while calculateLendingShares() plus amount-based withdraw paths kept creating more unrepresented assets through vault-favoring rounding.
    • Once those helpers had run, the protocol stopped treating the extra value as suspicious residue and started treating it as the real assets per share state of the pool.
  3. Sibling lending pools spent against that published truth

    • Puppet accounts could post the freshly inflated shares as collateral and immediately borrow stronger assets from other Wise pools.
    • This was the final and most dangerous promotion. Pool-local share accounting moved became protocol-wide borrow authority exists right now, which is exactly the jump a healthier design should have slowed, haircutted, or quarantined.

Pattern tags:

  • Below-throttle donation recognition: Wise did not fully discard attacker-authored balance growth; _cleanUp() still promoted some of it into canonical pool assets.
  • Iterative stealth-donation compounding: vault-favoring rounding on both deposit and amount-based withdrawal turned repeated loops into a share-price printing engine.
  • Concentrated-share collateral inflation: one actor could own the meaningful vault supply, then spread tiny collateral positions across puppet accounts to monetize the printed rate.
  • Cross-pool borrow extraction: the manipulated Pendle-LP wrapper mark only mattered because healthier sibling pools honored it as immediate borrow power.
  • Design companions: donation-share-price-manipulation, vault-share-truth-security-object, and collateral-admission-lag-and-borrow-side-quarantine.

Why the exploit worked

  • Wise’s anti-donation posture was incomplete: direct unsolicited balance growth below the throttle still entered recognized pool state through _cleanUp().
  • The pool’s share-conversion helpers then amplified that initial distortion. Deposit and withdrawal rounding both favored the vault, so repeated round-trips left behind unrepresented assets without minting or preserving proportional shares.
  • Low share supply made each leftover unit of value disproportionately important, so the attacker’s first donation and later stealth donations compounded instead of washing out.
  • The protocol did not neutralize those residual assets before reusing the resulting share price as collateral truth in other pools.
  • Once Wise let that locally printed wrapper rate cross into sibling lending markets, the vault bug and the lending bug became the same exploit surface.

Detection and audit pivots

  • Fuzz repeated deposit / withdraw loops under low-supply conditions and measure whether assets per share can ratchet upward without matched new share issuance.
  • Review every unexpected balance growth cleanup path, including supposedly conservative throttles. Ask what amount of donation is still recognized and how that recognition scales when supply is tiny.
  • Separate direct-donation defenses from stealth-donation defenses. Internal accounting can solve one while leaving the other intact.
  • Trace where wrapper marks become borrow authority. If a freshly inflated share rate can underwrite immediate borrowing from deeper pools, the downstream admission boundary is too weak.
  • Inspect attacker-operable ownership concentration paths: if one actor can cheaply dominate share supply, then later puppet-account collateralization and redeem-last extraction should be treated as first-class attack rails.

Failure-prevention checklist

  • Do not let cleanup logic partially recognize unsolicited balance growth just because it sits below a throttle. If the protocol wants a donation cap, the cap still needs a quarantine or explicit-recognition lane rather than small donations still reprice the pool.
  • Treat deposit rounding and withdraw-by-amount rounding as a single round-trip invariant. It is not enough for each leg to look conservative in isolation if repeated loops can leave net assets behind without minting or preserving proportional shares.
  • Add meaningful dead shares, virtual offsets, or another durable initial-liquidity buffer so a dust first position cannot dominate totalDepositShares and turn tiny leftovers into huge per-share moves.
  • Keep vault-local share appreciation from becoming immediate borrow power in sibling pools. Freshly appreciated wrapper marks should face borrow-side delay, caps, haircuts, or explicit quarantine until the protocol can distinguish earned yield from attacker-authored growth.
  • Separate unexpected-balance recognition from presentation and from collateral admission. A value increase that is still economically contestable should not simultaneously update share price, cashoutAmount, and cross-pool collateral value.
  • Fuzz full exploit pipelines, not only isolated helpers: seed with tiny supply, inject under-throttle donation, loop deposit/withdraw asymmetry, split collateral across puppet accounts, borrow from sibling pools, then redeem the concentrated residual position.
  • Monitor for impossible share-price appreciation. If lending share price or pseudoTotalPool / totalDepositShares jumps faster than the underlying Pendle LP can plausibly earn, the protocol should pause or quarantine before borrow-side extraction begins.

Audit prompts

  • What exact code path decides whether unsolicited balance growth is discarded, quarantined, or promoted into totalPool / pseudoTotalPool, and does any allowedDifference-style cap still let attacker-sized donations reprice a tiny-share pool?
  • Can a user loop deposit and amount-based withdraw operations so that each round leaves positive residual assets in the pool while the user’s share count does not grow proportionally?
  • Does the protocol prove a round-trip invariant across calculateLendingShares(), _preparationsWithdraw(), _cashoutAmount(), and any cleanup or sync helper, or are those helpers only locally conservative?
  • How cheaply can one actor dominate totalDepositShares in this pool, and what happens to assets-per-share if unexpected balances or rounding residue arrive while that concentration holds?
  • Which downstream modules consume the resulting share price as collateral, borrow power, liquidation value, or redemption authority, and what distrust boundary stops same-block monetization of freshly printed appreciation?
  • If a vault share price jumped 10x, 100x, or 5,000,000x relative to the opening deposit, what exact control would challenge or quarantine that move before healthier sibling pools honor it?
  • Can puppet-account splitting let an attacker externalize the borrow-side extraction first, then collapse the manipulated vault position afterward so the protocol keeps the bad debt?

Lessons that should already be standard

  • Internal accounting is not the same thing as donation neutrality. If any cleanup lane still promotes attacker-authored balances into recognized assets, the donation problem is only narrowed, not solved.
  • User-unfavorable rounding on both sides of a round trip is not automatically safe. In low-supply vaults it can become an attacker-controlled stealth-donation engine.
  • Vault share price becomes an oracle the moment sibling lending pools honor it as collateral. Local share math and cross-pool borrow policy are one security object.
  • Throttles are not quarantines. A rule that only limits the size of recognized unsolicited balance growth still lets an attacker print value when the denominator is tiny enough.
  • Dust-share states deserve launch-grade paranoia even in live systems. Once one actor can dominate the share base, leftover wei and rounding residue stop being negligible.
  • Wrapper-share collateral should season before it can recurse into broader borrowing. If the protocol cannot distinguish earned appreciation from attacker-authored appreciation, immediate borrowability is the design bug.

Internal linkages

  • Core recurring pattern: donation-share-price-manipulation.
  • Upstream share-accounting companion: vault-share-truth-security-object — useful when the sharper question is what one vault-side object should have kept issuance, rounding leftovers, and redemption meaning fused before any share-price path was exported.
  • Policy-boundary lens: entrypoint-only-policy-off-path-state-mutation — Wise tried to constrain explicit donation effects on the obvious path, but under-throttle direct donation plus later rounding-driven stealth donations still moved the assets-per-share state that sibling lending pools trusted.
  • Bootstrap cousin: first-deposit-inflation — Wise was not mainly about zeroing the next depositor, but the same tiny-initial-supply economics made the first under-throttle donation and later stealth-donation loops dramatically more powerful.
  • Publication-authority companion: sync-harvest-and-cache-write-authority-discipline — Wise had no single flashy syncRewards() button, but it still let attacker-authored exchange-rate growth cross too quickly from vault math into sibling lending collateral truth.
  • Positive-design downstream companion: collateral-admission-lag-and-borrow-side-quarantine — the deeper missed control was not only print-resistant exchange-rate math, but a borrow-side boundary that would have stopped a freshly inflated vault-share mark from becoming immediate debt capacity across the rest of the protocol.
  • Positive-design recursion companion: recursive-borrow-suppression-for-fresh-collateral — useful when the sharper question is how a healthy lender should have kept a freshly inflated wrapper mark from recursively monetizing into broader pool withdrawals before trust was earned.
  • Downstream payout rail: oracle-manipulation — useful when the sharper question is not how the share price was printed, but why sibling lending pools honored that printed price as borrowable truth.

Control surface