Polter Finance BOO Spot-Oracle Reserve-Ratio Manipulation (2024-11-16)

  • Name: Polter Finance BOO spot-oracle reserve-ratio manipulation
  • Date: 2024-11-16
  • Protocol: Polter Finance
  • Chain: Fantom
  • Category: oracle manipulation / thin-collateral lending exploit / oracle-adapter history-check failure
  • Loss: about 12 million in losses
  • Summary: Polter Finance forked Geist’s lending stack onto Fantom and let BOO serve as collateral through an AaveOracle -> PriceFeedV2 -> ChainlinkUniV2Adapter path that combined Chainlink data with live SpookySwap reserve ratios. The attacker flash-borrowed almost all BOO from the referenced SpookySwap v2 and v3 pools, leaving only 1e6 wei BOO in each pair, then deposited 1 BOO into Polter as collateral. Because the adapter’s getRoundData(roundId) ignored the requested round and returned the same current spot-reserve price as latestRoundData(), Polter’s intended previousChainlink0Response sanity check collapsed: both the current and previous prices were attacker-shaped spot reads from nearly empty pools. With the historical guard defeated, Polter valued that single BOO at about $1.373782984830617596e18, and the attacker borrowed out multiple deeper-liquidity markets before unwinding and laundering the proceeds.
  • Trigger / entry point:
    • The exploit centered on Polter’s BOO oracle path, especially ChainlinkUniV2Adapter at 0x875d564a6a86f6154592b88f7a107a517f00cc17 and PriceFeedV2 at 0x80663EDff11e99e8E0B34cb9C3E1fF32E82A80Fe.
    • Polter’s lending pools accepted BOO as collateral and relied on spot reserve ratios from SpookySwap BOO pairs instead of a manipulation-resistant valuation source.
    • The attack transaction was 0x5118df23e81603a64c7676dd6b6e4f76a57e4267e67507d34b0b26dd9ee10eac from exploit contract 0xA21451aC32372C123191B3a4FC01deB69F91533a.
  • Root cause:
    • Polter let a thin spot market define BOO collateral value inside a lending protocol that held much deeper, more borrowable assets.
    • The intended anti-spike guard in PriceFeedV2 depended on comparing the current BOO price against a prior round, but ChainlinkUniV2Adapter.getRoundData() did not return historical data at all; it reused the same live reserve-ratio read as latestRoundData().
    • That meant the protocol’s previousChainlink0Response was not historical truth. It was just the same manipulated spot price fetched a second time.
    • Once the attacker drained BOO reserves down to dust, the reserve-ratio-derived oracle made 1 BOO appear astronomically valuable and Polter honored that fiction as borrow power.
  • Exploit path:
    1. The attacker borrowed about 269,042 BOO from the referenced SpookySwap v3 pair 0xed23be0cc3912808ec9863141b96a9748bc4bd89 and about 1,154,788 BOO from the SpookySwap v2 pair 0xec7178f4c41f346b2721907f5cf7628e388a7a58, effectively emptying both BOO sides down to 1e6 wei.
    2. They deposited 1 BOO into Polter’s BOO lending market, creating the minimal collateral position needed to monetize the oracle distortion.
    3. During borrow validation, Polter’s AaveOracle asked PriceFeedV2 for BOO price. That contract in turn read the manipulated reserve ratios from the near-empty BOO pools.
    4. Because getRoundData(roundId) and latestRoundData() both returned the same current reserve-ratio-based result, the protocol’s previousChainlink0Response check failed open: the historical comparison path saw no meaningful difference and did not fall back to lastGoodPrice.
    5. Polter therefore valued 1 BOO at roughly $1,373,782,984,830,617,596, letting the attacker borrow real assets from several lending pools, beginning with about 9,134,844 wFTM and extending into other markets including MIM, WSOL, and bridged WETH liquidity.
    6. The attacker drained the victim pools, swapped and consolidated proceeds into FTM, split funds across many addresses, then bridged portions out through SquidRouter and LI.FI before some funds later moved through Tornado Cash on Ethereum.
  • What the attacker needed to be true:
    • BOO had to be accepted as collateral against much stronger assets.
    • The referenced SpookySwap BOO markets had to be thin enough that a flash borrower could collapse BOO reserves to dust.
    • The oracle path had to trust current reserve ratios rather than a manipulation-resistant price source or tightly bounded fallback.
    • The history check had to be fake history: previousChainlink0Response needed to resolve to another current spot read rather than an earlier round.
    • Borrow caps and collateral constraints on the BOO market had to remain loose enough for one fabricated price to drain multiple sibling pools.
  • Technical anatomy worth remembering:
    • This was not only a thin market failure. It was a composition failure between a manipulable reserve-ratio oracle and a broken historical-sanity-check implementation.
    • The subtle bug worth preserving is that the protocol appeared to compare current versus previous data, but both code paths ultimately returned the same live spot-derived answer.
    • Polter is a strong oracle-manipulation exemplar because the collateral asset was not Polter’s own token; it was an external ecosystem token whose local DEX depth still sat far below the value Polter let users borrow against it.
    • The case is also a reminder that forked code plus inherited audit branding do not cover newly introduced oracle glue. Polter’s own audit page told users the contracts were identical to Geist, except for the removal of the flash-loan function, yet the BOO pricing stack (AaveOracle -> PriceFeedV2 -> ChainlinkUniV2Adapter) still introduced protocol-specific solvency logic outside that reassurance story.

Pattern tags

Three thin-collateral price-authority promotions this incident collapsed

  1. A local BOO spot-reserve state exists at all

    • The referenced SpookySwap v2 and v3 pools still had some BOO balance and Polter’s adapter stack could observe a fresh reserve-ratio-derived price from them.
    • This is only the existence question: there is some market-shaped state object the protocol might later read.
  2. That local spot state deserves to count as canonical and historical oracle truth for Polter

    • Polter still had to prove that a near-empty BOO pool was deep and independent enough to price lending collateral, and that its previousChainlink0Response branch really consumed earlier state instead of another read of the manipulated present.
    • This is the oracle-truth question: this local reserve state really deserves to become current and previous price meaning for BOO, not merely the adapter returned a number twice.
  3. That accepted oracle truth deserves immediate authority over deeper sibling reserves

    • Even after BOO was accepted at an absurd mark, Polter still had to decide how much wFTM, MIM, WSOL, and bridged WETH that one collateral fact could release before any second boundary engaged.
    • This is the authority-budget question: this accepted BOO price really deserves this much same-window borrow authority across the rest of the deployment.
  • Representative transactions and addresses:
    • Main exploit tx: 0x5118df23e81603a64c7676dd6b6e4f76a57e4267e67507d34b0b26dd9ee10eac
    • Exploiter EOA: 0x511f427Cdf0c4e463655856db382E05D79Ac44a6
    • Exploit contract: 0xA21451aC32372C123191B3a4FC01deB69F91533a
    • SpookySwap v2 BOO pair: 0xec7178f4c41f346b2721907f5cf7628e388a7a58
    • SpookySwap v3 BOO pair: 0xed23be0cc3912808ec9863141b96a9748bc4bd89
    • ChainlinkUniV2Adapter: 0x875d564a6a86f6154592b88f7a107a517f00cc17
    • PriceFeedV2: 0x80663EDff11e99e8E0B34cb9C3E1fF32E82A80Fe
    • Victim pools highlighted by CertiK: pMIM 0xa826b29d81caef8c9aa212f172ab3ef00566e91e, pWSOL 0x0299553df0fa396c0f6f3456d293608e189c3cf3, pFTM 0xbbce4b1513d4285bd7a59c2c63835535151c8e7c, pLZ_WETH 0x328c7a684f160c089ebff07ff1b5a417f024979e
  • Response / mitigation:
    • Polter paused the platform after the exploit and publicly began tracing funds.
    • The team attempted to notify bridges and later negotiated onchain with the exploiter while some funds were already being split and bridged away.
    • The incident also drew scrutiny to Polter’s security posture because its docs presented Geist audit reports and stated the contracts were identical to Geist except for flash-loan removal, even though the BOO oracle path carried protocol-specific assumptions that deserved their own review.

Why the exploit worked

  • Polter collapsed a local BOO spot state exists, that spot state still counts as independent oracle truth, and that accepted truth may underwrite borrowing from deeper sibling reserves into one unchecked path. Once the attacker drained BOO reserves to dust, the lending engine still treated that local market state as money-good collateral truth.
  • The fake-history guard was the decisive trust-boundary failure. previousChainlink0Response looked like a second, time-separated fact, but getRoundData(roundId) resolved to another read of the same manipulated present-state reserve ratio. The protocol therefore had apparent redundancy without actual independence.
  • The borrowed value lived somewhere else. BOO itself was thin and easy to distort, but Polter let that thin market’s printed price reach much deeper pools holding wFTM, MIM, WSOL, and bridged WETH. The exploit paid because local collateral fiction was granted cross-pool reserve authority.
  • The fork story weakened review discipline. By presenting the deployment as essentially Geist-without-flash-loans, Polter encouraged reviewers to inherit confidence from upstream core contracts while the real new solvency boundary sat in protocol-specific oracle glue and collateral configuration.

Detection and audit pivots

  • Treat previous oracle branches as code paths to falsify, not comfort labels to trust. Polter’s strongest paper defense was the previousChainlink0Response comparison, but the real question was whether getRoundData(roundId) ever returned time-separated state at all.
  • Review reserve-ratio adapters as solvency code, not price-decoration code. Once BOO’s reserve balance on both referenced SpookySwap pools could be flash-borrowed down to 1e6 wei, the adapter was really letting a dust reserve state set lending-market collateral truth.
  • Trace where fake diversification collapses. Polter appeared to have both current and historical checks plus two BOO liquidity venues, but the practical security question was narrower: how many genuinely independent, manipulation-resistant facts were left after both current and previous branches resolved to live spot reads and both venues referenced the same thin token float?
  • Ask how much value one absurd collateral mark can immediately reach. The exploit only needed 1 BOO of posted collateral because borrow validation had line-of-sight into several deeper-liquidity sibling pools once the oracle accepted the fabricated mark.
  • Audit fork claims at the changed edges. Same as Geist except flash loans removed was never the right review boundary once Polter added or reconfigured a BOO-specific oracle stack that upstream Geist did not need to underwrite safely.

Failure-prevention checklist

  • Require historical guards to consume genuinely time-separated state. If a contract exposes getRoundData(roundId) or similar chronology APIs, test that changing roundId changes the returned observation set and timestamp under adversarial conditions.
  • Do not let reserve-ratio adapters for thin pools create full borrow power against deeper-liquidity assets. If such feeds exist at all, isolate the market, apply severe haircuts, and cap cross-pool borrow reach to what the collateral can plausibly liquidate at size.
  • Treat external ecosystem tokens like BOO as fresh collateral listings, not inherited safe assets, even inside forked lending systems. Upstream audits do not transfer automatically to new oracle adapters, new pool selections, or new collateral-factor decisions.
  • Couple fake-history detection to circuit breakers. If current, previous, and fallback paths collapse onto the same live state read, borrowing should pause or the market should fall back to a much harsher collateral policy immediately.
  • Bound blast radius per collateral market. One manipulated asset should not be able to drain multiple sibling reserves before responders can freeze the market.
  • Keep explicit dependency maps for every oracle branch: which pair, which reserve read, which adapter, which historical store, which fallback, and which operator assumptions each one depends on.

Audit prompts

  • Does the supposedly historical branch actually read stored historical data, or does it just recompute the current spot state through a different function name?
  • If an attacker flash-borrows almost all of the token from the referenced pools, what exact value does the adapter publish, and how much borrowing power does that create before any fallback triggers?
  • Are the protocol’s current, previous, fallback, and lastGoodPrice paths genuinely independent enough that one manipulated reserve state cannot satisfy all of them at once?
  • What liquidation depth exists for the collateral at the size the protocol is willing to honor, and how does that compare with the strongest asset pools the borrower can drain against it?
  • Which security-critical components differ from the upstream fork the docs cite as audited: collateral listings, adapter composition, pool selection, decimals / reserve math, or guard logic?
  • If one collateral mark becomes absurd for one block, what hard caps, delays, or per-market pauses stop that bad price from reaching every borrowable reserve in the same deployment?

Lessons that should already be standard

  • Historical checks without historical state are theater. A previous getter that just returns the current manipulated answer is not defense in depth; it is duplicated failure.
  • Current spot state exists is not the same claim as historical guard passed, and even historical guard passed is not the same claim as deep sibling pools may now honor this mark immediately.
  • Thin local spot markets should not be allowed to underwrite deep balance sheets. The right comparison is not does the feed return a number, but could the protocol actually liquidate the collateral anywhere near that value.
  • Fork equivalence claims stop at the first protocol-specific risk adapter. Reusing an upstream lending core does not inherit safety for new collateral, new oracle glue, or new market-configuration assumptions.
  • Multi-venue language can hide one shared float. Two BOO pairs were still mostly one manipulable collateral universe once both venues could be emptied in the same flash-loan sequence.
  • Blast radius is part of oracle design. If one fabricated collateral mark can immediately reach many deeper-liquidity pools, the oracle problem is already a treasury-containment problem.

Why this incident matters

  • Polter broadens the oracle-manipulation corpus with a clean thin external collateral + broken oracle guard branch, distinct from protocol-native-token pumps like Moola and from LP-wrapper collateral cases like Cheese Bank.
  • It preserves a useful lesson about fake defense-in-depth: a fallback or historical-price check is meaningless if the supposedly independent data path resolves to the same manipulable spot state.
  • The case is a sharp reminder that oracle adapter code is where many forks stop being the upstream protocol they copied, even when docs still market the deployment as effectively the same audited system.

Internal linkages

Control surface

  • The decisive leverage sat in BOO collateral admission, oracle venue selection, reserve-ratio math inside ChainlinkUniV2Adapter, the fake-history guard in PriceFeedV2, and the borrow caps that let one fabricated collateral mark reach multiple pools.
  • Polter did not fail because BOO was volatile. It failed because a cheap-to-print local spot state was allowed to masquerade as historical, independent, solvency-grade truth.

Sources

Last reviewed

  • 2026-07-29 UTC