Reproduced Exploit
CompoundFork (Pike Finance "uSUI") Exploit — Spot-Price Oracle Manipulation of a Compound v2 Fork
A Compound-v2 fork on Base (the "Pike"/uSUI markets) priced its uSUI collateral via a custom price feed 0xc112…7e0c that reads the instantaneous slot0().sqrtPriceX96 of an Aerodrome Slipstream CL pool. Spot price in a concentrated-liquidity AMM is freely movable inside a single transaction by swapp…
Loss
~$1M total protocol drain; the reproduced WETH leg = 256.05 WETH (≈ $632K @ ~$2,470/ETH)
Chain
Base
Category
Oracle Manipulation
Date
Oct 2024
Source & credit. Exploit reproduction, trace data, and analysis adapted from DeFiHackLabs by SunWeb3Sec — an open registry of reproduced on-chain exploits. Standalone Foundry PoC and full write-up: 2024-10-CompoundFork_exploit in the
evm-hack-registrymirror. Upstream DeFiHackLabs PoC:src/test/…/CompoundFork_exp.sol.
Reproduction: the PoC compiles & runs in an isolated Foundry project at this project folder (the umbrella DeFiHackLabs repo contains many unrelated PoCs that fail a whole-repo build, so this one was extracted). Full verbose trace: output.txt. The lending protocol's contracts (oracle, comptroller, cTokens) are unverified on Basescan; the verified-source analysis below is for the manipulable price source — an Aerodrome Slipstream CL pool (sources/CLPool_eC8E53) — and the Chainlink ETH/USD aggregator (sources/EACAggregatorProxy_71041d). The oracle's pricing logic is fully reconstructed from the on-chain call trace.
Key info#
| Loss | ~$1M total protocol drain; the reproduced WETH leg = 256.05 WETH (≈ $632K @ ~$2,470/ETH) |
| Vulnerable contract | Price oracle 0x93D6…dD5a — 0x93D619623abc60A22Ee71a15dB62EedE3EF4dD5a (unverified) — uses Aerodrome CL pool slot0() spot price for the uSUI feed |
| Victim markets | cWETH 0x5c52…8aF8 and cSUI 0xA209…551B of a Compound-v2 fork; Comptroller 0xf91d…43D3 |
| Manipulated pool | Aerodrome Slipstream WETH/uSUI CL pool 0x5C45b0F48c326f79b56709d8F63CE2beE7697106 (impl 0xeC8E…5831) |
| Attacker EOA | 0x81d5187c8346073b648f2d44b9e269509513aae2 |
| Attacker contract | 0x7562846468089cf0e8f7b38ac53406b895284901 |
| Attack tx | 0x6ab5b7b51f780e8c6c5ddaf65e9badb868811a95c1fd64e86435283074d3149e |
| Chain / block / date | Base / 21,512,062 / 2024-10-24 23:44 UTC |
| Flash-loan source | Morpho Blue 0xBBBB…FFCb, 800 WETH, fee-free (onMorphoFlashLoan) |
| Compiler | PoC: Solidity ^0.8.0; pool source =0.7.6 |
| Bug class | DeFi price-oracle manipulation — lending oracle uses a manipulable AMM spot price (slot0().sqrtPriceX96) instead of a TWAP |
TL;DR#
A Compound-v2 fork on Base (the "Pike"/uSUI markets) priced its uSUI collateral via a custom
price feed 0xc112…7e0c that reads the instantaneous slot0().sqrtPriceX96 of an Aerodrome
Slipstream CL pool. Spot price in a concentrated-liquidity AMM is freely movable inside a single
transaction by swapping against (or simply re-pricing) the pool.
The attacker, inside a single 800-WETH Morpho flash loan:
- Drained the cheap collateral first — deposited 15 WETH as
cWETHcollateral, entered the market, and borrowed the entire 13,982.87 uSUI sitting in thecSUImarket while uSUI was still cheap (oracle price ≈ 1.95). - Pumped the uSUI oracle price — swapped WETH into the WETH/uSUI CL pool with
sqrtPriceLimitX96 = 1000e18, pushing the pool'sslot0price to the limit. The lending oracle's reported uSUI price jumped from 1.9457e18 → 2.53e27, a ~1.3 billion-x inflation. 2.b Bought 432,241 uSUI in that same swap. - Re-deposited the (now astronomically valued) uSUI — minted
cSUIwith 50 uSUI; with the inflated oracle,getAccountLiquiditynow reported enormous collateral value. - Borrowed the entire
cWETHmarket — drew 262.44 WETH (everything the cWETH market held) against the fake collateral. - Unwound — dumped all uSUI back into the pool for 357.95 WETH, repaid the 800-WETH flash loan, and walked off with the difference.
Net WETH profit (reproduced): 256.05 WETH. The bad debt left behind (un-repaid cWETH + cSUI borrows against worthless inflated collateral) is what makes the total protocol loss ≈ $1M.
Background — what the protocol does#
The lending protocol is a textbook Compound v2 fork (Comptroller + CErc20 cTokens behind
upgradeable delegators):
- Comptroller
0xf91d…43D3(impl0x94A9…e943) — risk engine.enterMarkets,borrowAllowed, andgetAccountLiquidityall multiply each cToken balance by the oracle'sgetUnderlyingPrice(cToken)and the market's collateral factor to decide whether an account is solvent enough to borrow. - cTokens —
cWETH0x5c52…8aF8andcSUI0xA209…551B, both delegators pointing at the sameCErc20Delegateimplementation0x37b6…9e4f. Standardmint(supply),borrow,getAccountSnapshot. - Price oracle
0x93D6…dD5a(the address the PoC labelspitfalls) — forcSUI, it resolves the underlying (uSUI0xb050…6ea4) and reads its USD price from a custom feed0xc112…7e0c. That feed'slatestAnswer()is the broken component: it readsslot0()of the Aerodrome WETH/uSUI CL pool0x5C45…Bb70(i.e. the live spot price) and combines it with a Chainlink ETH/USD answer to produce a USD price for uSUI.
The collateral asset uSUI therefore had a price that any swapper could move at will within one block.
The Aerodrome Slipstream CL pool is a Uniswap-V3-style concentrated-liquidity pool. Its slot0
holds the current sqrt-price:
// sources/CLPool_eC8E53/contracts_core_CLPool.sol:53-69
struct Slot0 {
// the current price
uint160 sqrtPriceX96;
int24 tick;
uint16 observationIndex;
uint16 observationCardinality;
uint16 observationCardinalityNext;
bool unlocked;
}
Slot0 public override slot0;
slot0.sqrtPriceX96 is overwritten on every swap (CLPool.sol:833),
so reading it returns the price as of the last swap in the block — fully attacker-controlled.
The vulnerable code#
1. The oracle reads the AMM spot price (reconstructed from the trace)#
The oracle 0x93D6…dD5a is unverified, but its call tree is unambiguous. For getUnderlyingPrice(cSUI)
(output.txt:118-153) it does:
oracle.getUnderlyingPrice(cSUI)
├─ cSUI.symbol() → "cSUI"
├─ cSUI.underlying() → uSUI (0xb050…6ea4)
├─ feed(0xc112…).decimals() → 8
└─ feed(0xc112…).latestAnswer()
├─ pool.token0() → WETH (0x4200…0006)
├─ pool.slot0() → sqrtPriceX96 ← ⚠️ LIVE SPOT PRICE
├─ ethUsdAgg.latestAnswer() → 0x0b990696 (ETH/USD, 8 dp)
└─ uSUI.decimals() → 18
→ returns 1_945_780_700_000_000_000 (1.9457e18) [before manipulation]
The single line that matters is the slot0() read on the CL pool. There is no TWAP, no observation
window, no bound check — the feed trusts the instantaneous sqrt-price.
2. The spot price is overwritten by any swap#
// sources/CLPool_eC8E53/contracts_core_CLPool.sol — inside swap()
// after the swap step loop completes:
slot0.sqrtPriceX96 = state.sqrtPriceX96; // :833 ← new spot price persisted
So a swap with a chosen sqrtPriceLimitX96 lets the caller dictate the post-swap spot price the
oracle will read on the very next call in the same transaction.
3. The Comptroller solvency check trusts the oracle linearly#
getAccountLiquidity (output.txt:564-609) and borrowAllowed both compute
collateralValue = Σ (cTokenBalance · exchangeRate · collateralFactor · getUnderlyingPrice(cToken)).
With the uSUI price inflated ~1.3e9×, 50 uSUI of cSUI collateral is valued as if it were billions of
dollars, so the account is reported as massively over-collateralised and may borrow the entire cWETH
market.
Root cause — why it was possible#
The lending oracle derives a collateral asset's price from a manipulable AMM spot price (
slot0().sqrtPriceX96of a single CL pool), and the risk engine trusts that price linearly when sizing borrows. Spot price in an AMM is not a price feed — it is the result of the last trade and can be moved arbitrarily within one transaction.
The composing decisions:
- Spot, not TWAP. The custom uSUI feed
0xc112…7e0creadsslot0()directly. A Uniswap-V3-style pool exposesobserve()/cumulative ticks precisely so integrators can compute a manipulation- resistant TWAP; this feed ignored them and used the live price. - Thin / single-pool liquidity. The WETH/uSUI CL pool was shallow enough that ~349 WETH plus a
chosen
sqrtPriceLimitX96 = 1000e18slammed the price to the limit, yielding a 1.3-billion-x oracle move. - No sanity bounds on the oracle output. No min/max price, no deviation check vs. a second source, no circuit breaker. A price that jumps nine orders of magnitude in one block was accepted verbatim.
- Order of operations is irrelevant to the attacker. Because both the borrow check and the collateral valuation read the same manipulable price, the attacker can first borrow the under-priced asset (uSUI), then inflate the price and borrow against it — extracting both sides.
Preconditions#
- The cSUI market holds borrowable uSUI (13,982.87 uSUI at the fork block) and the cWETH market holds borrowable WETH (262.44 WETH) — both are drained.
- The uSUI price feed reads a CL pool whose
slot0the attacker can move; the pool must be thin enough that the available capital moves it to the chosen limit. - Capital to (a) post a token of cWETH collateral, (b) buy uSUI to pump the pool. All of it is flash-loanable — the PoC takes an 800-WETH, fee-free Morpho flash loan and repays it in full.
- The fork block (Base 21,512,062) requires a Base archive RPC; Infura returned
error 4444: pruned history unavailable, so the project uses the publichttps://mainnet.base.org.
Attack walkthrough (with on-chain numbers from the trace)#
All figures are taken directly from the call/Swap events in output.txt.
WETH = 0x4200…0006, uSUI = 0xb050…6ea4. cToken underlying prices are the oracle's
getUnderlyingPrice returns (1e18-scaled, but cross-decimal so the absolute number is only meaningful
relatively).
| # | Step | Trace | Key quantities |
|---|---|---|---|
| 0 | Flash loan 800 WETH from Morpho | :26 | +800 WETH |
| 1 | weth.approve(cWETH), cWETH.mint(15 WETH) → post 15 WETH as collateral | :46 | 15 WETH locked |
| 2 | enterMarkets([cSUI]) | :90 | cSUI = collateral mkt |
| 3 | Borrow ALL uSUI from cSUI while uSUI is cheap (oracle uSUI ≈ 1.9457e18) | :105 | +13,982.87 uSUI drained from cSUI mkt |
| 4 | Move WETH + uSUI to Helper, run Helper.d() | :92-99 | — |
| 5 | Pump pool: exactInputSingle(WETH→uSUI, sqrtPriceLimitX96 = 1000e18) | :283, Swap :394 | spends 349.33 WETH, gets 432,241 uSUI; pool sqrtPriceX96 → 1000e18 |
| 5.b | Oracle now reports uSUI price = 2.5325e27 (was 1.9457e18) | :475 | ~1.3e9× inflation |
| 6 | cSUI.mint(50 uSUI) — deposit a sliver of uSUI as collateral | :512 | 50 uSUI → cSUI |
| 7 | getAccountLiquidity confirms huge fake collateral | :564 | "rich on paper" |
| 8 | Borrow ALL WETH from cWETH against fake collateral | :610 | +262.44 WETH drained from cWETH mkt |
| 9 | Dump uSUI back: exactInputSingle(uSUI→WETH) | :739, Swap :858 | sells 446,174 uSUI → +357.95 WETH |
| 10 | Helper sends 1,056.05 WETH back to attack contract; selfdestruct | :938 | — |
| 11 | Repay 800-WETH flash loan (transferFrom → Morpho) | :951 | −800 WETH |
| 12 | Transfer remainder to test runner | :960 | 256.05 WETH profit |
Oracle price before vs. after the pump#
uSUI oracle price (getUnderlyingPrice(cSUI)) | pool sqrtPriceX96 | |
|---|---|---|
| Before pump (:118-153) | 1,945,780,700,000,000,000 (1.9457e18) | 2,858,318,274,747,956,646,160,580,417,568 |
| After pump (:475-511) | 2,532,537,576,160,000,000,000,000,000 (2.5325e27) | 1,000,000,000,000,000,000,000 (= the chosen limit, 1000e18) |
| Factor | ≈ 1.30 × 10⁹ | price reset to the swap limit |
Profit accounting (WETH)#
| Direction | Amount (WETH) | Source |
|---|---|---|
| Flash-loan in | +800.00 | Morpho :26 |
Collateral deposit (cWETH.mint) | −15.00 | :46 |
| Pump swap (WETH→uSUI) | −349.33 | Swap :394 |
| Borrow from cWETH market | +262.44 | :610 |
| Dump swap (uSUI→WETH) | +357.95 | Swap :858 |
| Flash-loan repay | −800.00 | :953 |
| Net profit | +256.05 | test runner balance :960 |
Reconciliation: starting from the 800 WETH loan, the attacker ends with
800 − 15 − 349.33 + 262.44 + 357.95 = 1,056.06WETH in hand, repays 800, nets 256.05 WETH. The 13,982.87 uSUI it also borrowed from cSUI is part of what it dumped (along with the 432,241 uSUI bought) in the final swap. The protocol is left holding the attacker's worthless 50-uSUI position as "collateral" against two fully-drained markets — the ~$1M bad debt.
Diagrams#
Sequence of the attack#
Pool / oracle state evolution#
Where the trust breaks (oracle data flow)#
Remediation#
- Never price collateral from AMM spot. Replace the
slot0()read in feed0xc112…7e0cwith a manipulation-resistant TWAP (pool.observe()over a multi-minute window) or, better, a Chainlink/redundant feed for uSUI. SpotsqrtPriceX96must never feed a lending solvency check. - Bound and cross-check oracle output. Reject prices that deviate beyond a sane band from a second independent source, and add absolute min/max sanity limits. A 1.3-billion-x intra-block move should trip a circuit breaker, not be accepted.
- Require deep, multi-source liquidity before listing a collateral. uSUI was priced off a single thin CL pool; a few hundred WETH moved its price nine orders of magnitude. Collateral assets need liquidity depth commensurate with the borrowable value behind them, ideally across multiple venues.
- Use cumulative-price observations the pool already exposes. The CL pool stores observations for exactly this purpose; integrators should read TWAPs, not the instantaneous tick.
- Cap per-block oracle movement / add update delays. Even with a TWAP, freeze borrows when the reference price jumps abnormally within a short window.
How to reproduce#
The PoC was extracted into a standalone Foundry project (the umbrella DeFiHackLabs repo has many
unrelated PoCs that fail under a whole-repo forge build):
_shared/run_poc.sh 2024-10-CompoundFork_exploit -vvvvv
- RPC: a Base archive endpoint is required. Infura/most public Base RPCs prune block 21,512,062 and
fail with
error 4444: pruned history unavailable;foundry.tomltherefore uses the public archivehttps://mainnet.base.org(drpc.org also has the depth but rate-limits with HTTP 429 mid-fork). - Dependencies copied into the project root so
import "../basetest.sol"resolves: basetest.sol (which imports tokenhelper.sol). - Result:
[PASS] testExploit()with an attacker WETH balance going from 0 → 256.054617598590406175.
Expected tail:
Attacker Before exploit WETH Balance: 0.000000000000000000
Attacker After exploit WETH Balance: 256.054617598590406175
...
Ran 1 test for test/CompoundFork_exploit.sol:CompoundFork
[PASS] testExploit() (gas: ...)
Suite result: ok. 1 passed; 0 failed; 0 skipped
References: PoC header — Phalcon explorer tx 0x6ab5b7b5…d3149e; @Phalcon_xyz thread
https://x.com/Phalcon_xyz/status/1849636437349527725 (Base, "uSUI"/Compound-fork, ~$1M).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 2024-10-CompoundFork_exploit (evm-hack-registry mirror).
- Upstream DeFiHackLabs PoC:
CompoundFork_exp.sol. - Attack transaction: view on explorer.
Alerts & third-party analyses
- Original alert / thread: post on X.
- DeFiHackLabs incident explorer: search "CompoundFork (Pike Finance "uSUI") Exploit".
- Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.