# WhaleBit CES / IGT Staking Spot-Oracle Manipulation

On March 31, 2026 at 22:56:21 UTC (Polygon block `84938872`), an attacker exploited WhaleBit’s unverified staking system through a **same-transaction spot-oracle manipulation** funded by a flash loan. The attacker EOA `0xe66b37de57b65691b9f4ac48de2c2b7be53c5c6f` used helper contract `0xb5a8d7a37d60aa662f4dc9b3ef4c32a3fe21fadf` to borrow `51,024.905390945780848543 CES`, run three batches of five helper deposits into the staking path, dump CES into the WhaleBit CES/USDT0 Algebra-style pool to depress the same-block spot price, redeem previously minted IGT for inflated CES payouts, unwind the market leg, and repay the loan. The finding is revalidated: the root cause is not the flash loan itself, but a staking redemption path that trusts a live pool spot price through the WhaleBit pricing module while the attacker can still move that pool inside the same transaction. After repaying `51,177.980107118618191089 CES`, the exploit executor retained `10,506.125286809215449705 CES`.

## Root Cause

The price module ( `0xb5ea1d17f3d8da34a6d6a1d2acc2a148e1411868` → impl `0x0729ea061132bcd76f420f9139af8957b41b90cb`, decompiled by Dedaub) exposes a `deposit` and a `withdraw` function. Both read `getActualPrice()` — the **live spot price** of the CES/USDT0 Algebra pool — to convert between CES and IGT. Because an attacker can move that spot price by swapping against the pool inside the same transaction, they can deposit at a fair price, depress the spot, and withdraw at the manipulated price to extract the difference.

### The Vulnerable Code

`getActualPrice()` returns the live, manipulable spot price (price module impl, Dedaub decompile):

“`
// selector 0x6cc09081 function getActualPrice() external returns (uint256) { return IDexAdapter(_dexAddress).getActualPrice(); // live spot from the Algebra pool }
“`

`getPrice()` (selector `0x98d5fdca`) also exists and returns an **average** price — but it is only used as a sanity reference in the deviation guard, never in any calculation.

`deposit` mints IGT using the spot price (price module impl, selector `0xcaddba3d`):

“`
function deposit(address depositor, uint256 cesAmount) external returns (uint256) { uint256 spotPrice = getActualPrice(); // live spot _checkDeviation(getPrice(), spotPrice); uint256 igtAmount = cesAmount * spotPrice / 1e18; // ← spot drives the mint rate IGT.mint(msg.sender, igtAmount); return igtAmount; }
“`

At pre-manipulation spot **P**: `IGT = CES × P / 1e18`

`withdraw` releases CES using the spot price — same read, inversely applied (price module impl, selector `0xa527aed6`):

“`
function withdraw(address recipient, uint256 igtAmount) external returns (uint256) { uint256 spotPrice = getActualPrice(); // live spot — depressed by the attacker at this point _checkDeviation(getPrice(), spotPrice); uint256 cesAmount = igtAmount * 1e18 / spotPrice; // ← lower spot → MORE CES out IGT.burn(recipient, igtAmount); CES.transfer(recipient, cesAmount); return cesAmount; }
“`

At post-manipulation spot **P’** (P’ < P): `CES_out = IGT × 1e18 / P’`

**Why this is profitable:**

“`
Step 1 — deposit at fair price P: IGT = CES_in × P / 1e18 Step 2 — attacker dumps CES, spot drops to P’ = 0.8645 × P Step 3 — withdraw at depressed price P’: CES_out = IGT × 1e18 / P’ = CES_in × P / P’ (substituting IGT from step 1) Profit = CES_out − CES_in = CES_in × (P / P’ − 1) = CES_in × (1 / 0.8645 − 1) ≈ CES_in × 15.7%
“`

**The deviation guard existed but the threshold was too permissive** (price module impl):

“`
function _checkDeviation(uint256 avgPrice, uint256 spotPrice) internal view { uint256 deviation; if (avgPrice <= spotPrice) { deviation = (spotPrice – avgPrice) * 10000 / avgPrice; } else { deviation = (avgPrice – spotPrice) * 10000 / spotPrice; // ← fires during withdrawal } require(deviation <= _maxPriceDeviation, “Price deviation too high”); }
“`

At the observed 13.55% spot drop, the else-branch yields `(1 − 0.8645) / 0.8645 × 10000 ≈ 1567 bps`. The check passed because `_maxPriceDeviation ≥ 1567 bps`. A TWAP-based oracle or a tighter threshold would have reverted the withdrawal.

### Vulnerable Contracts

RoleProxyImplementationStaking (entry point)Price module (root cause)Oracle pool (manipulated)—

## Attack Execution

### High-Level Flow

1. The attacker EOA calls helper `0xb5a8d7a37d60aa662f4dc9b3ef4c32a3fe21fadf`(top-level selector `0x2512b5d8`).
2. The helper flash-borrows `51,024.905390945780848543 CES` from `0x296b95dd0e8b726c4e358b0683ff0b6d675c35e9` via `flash(address,uint256,uint256,bytes)`( `0x490e6cbc`).
3. Inside `uniswapV3FlashCallback(uint256,uint256,bytes)`( `0xe9cbafb0`), the helper runs **three batches**. Each batch consists of:
– five helper deposit loops into the staking proxy,
– one large CES dump into the CES/USDT0 oracle pool,
– five helper redemption loops,
– one unwind swap that buys CES back with USDT0.
4. After the third batch, the helper repays the flash loan principal plus fee and keeps the residual CES.

### Detailed Call Trace

The trace below is condensed from `trace_callTracer.json` and focuses on the price-sensitive path.

“`
EOA 0xe66b37de57b65691b9f4ac48de2c2b7be53c5c6f -> ExploitExecutor 0xb5a8d7a37d60aa662f4dc9b3ef4c32a3fe21fadf [0x2512b5d8] -> CES/USDT0 flash pool 0x296b95dd0e8b726c4e358b0683ff0b6d675c35e9.flash(…) [0x490e6cbc] -> CES.transfer(ExploitExecutor, 51024.905390945780848543) -> ExploitExecutor.uniswapV3FlashCallback(…) [0xe9cbafb0] [BATCH 1: five helper deposits] -> helper.deposit-wrapper -> staking proxy.deposit(uint256) [0xb6b55f25] x5 -> staking proxy DELEGATECALL impl 0x9153… -> accounting proxy.getPriceForLevel(…) [0x2266fc92] -> accounting proxy.getBalance(…) [0xf8b2cb4f] -> price module.getPrice() [0x98d5fdca] ← reads average price (deviation guard only) -> price module.getActualPrice() [0x6cc09081] ← reads SPOT price -> price module.deposit(addr,amt) [0xcaddba3d] ← mints IGT = CES * spotPrice / 1e18 -> accounting proxy.paymentDepositTo(…) [0xe06cbd2b] -> IGT minted to staking proxy -> oracle pool 0xd3a933….swap(…) [0x128acb08] -> algebraSwapCallback(…) on ExploitExecutor [0x2c8958f6] -> executor sends 161683.584248230639260623 CES -> executor receives 163524.673671 USDT0 -> ExploitExecutor -> helper.withdraw() [0x3ccfd60b] x5 -> helper -> staking proxy [0x2e1a7d4d] x5 -> staking proxy DELEGATECALL impl 0x9153… -> accounting proxy.getBalance(…) [0xf8b2cb4f] -> price module.igt() [0x1162d512] -> IGT.balanceOf(staking proxy) -> price module.getPrice() [0x98d5fdca] ← reads average (guard only) -> price module.getActualPrice() [0x6cc09081] ← reads SPOT (now depressed ~13.55%) -> accounting proxy.updateBalance(…) [0xe0b1cccb] -> CES.transfer(helper, redeemed amount) -> price module.withdraw(addr,amt) [0xa527aed6] ← burns IGT, releases CES = IGT * 1e18 / spotPrice -> oracle pool unwind swap -> executor sends 163524.673671 USDT0 -> executor receives 160650.278584760338216069 CES [BATCH 2] -> same five deposit loops -> dump 165017.905023638276437724 CES for 166650.549328 USDT0 -> same five helper redemption loops -> unwind 166650.549328 USDT0 for 164056.172947708879699775 CES [BATCH 3] -> same five deposit loops -> dump 168605.838479456084319365 CES for 169906.374332 USDT0 -> same five helper redemption loops -> unwind 169906.374332 USDT0 for 167594.376546131873840784 CES -> ExploitExecutor repays flash pool -> CES.transfer(0x296b95dd0e8b726c4e358b0683ff0b6d675c35e9, 51177.980107118618191089)
“`

The redemption uplift per batch is directly visible from the per-helper CES-in vs CES-out figures:

BatchCES into price path per helperCES redeemed per helperUplift123

The spot price depression responsible for the uplift is confirmed by sequential `sqrtPriceX96` snapshots forwarded through the DEX adapter to the Algebra pool’s `globalState()`:

SnapshotImplied price vs openOpening in-txLowest in-txHighest in-tx

Using `price ~ sqrtPriceX96²`, the trough is **13.55% below** the opening spot — consistent with the observed redemption uplifts of 14.4%–15.7% across the three batches.

### Batch-Level Token Flow

The batch totals derived from `funds_flow.json` are:

BatchCES into staking price pathIGT minted/burnedCES dump into oracle poolUSDT0 receivedCES redeemed from stakingCES from unwind123

Across all three batches:

– total IGT minted and later burned: `97,366.64127003649 IGT`
– total CES routed into the staking price path: `90,890.144365474106696073 CES`
– total CES redeemed from the staking path: `104,555.844041180056907725 CES`

That net uplift on the staking leg is what ultimately funds the final profit after the flash-loan fee.

## Financial Impact

### Flash-Loan Leg

– Flash principal borrowed: `51,024.905390945780848543 CES`
– Flash repayment: `51,177.980107118618191089 CES`
– Flash fee: `153.074716172837342546 CES`

### Oracle-Manipulation Leg

Aggregate oracle-pool swaps across the three batches:

– CES dumped into oracle pool: `495,307.327751325000017712 CES`
– USDT0 received from oracle pool: `500,081.597331 USDT0`
– USDT0 spent to unwind: `500,081.597331 USDT0`
– CES recovered on unwind: `492,300.828078601091756628 CES`

### Final Profit

The exploit executor’s net balance change in `funds_flow.json` is:

– `+10,506.125286809215449705 CES`
– `0 USDT0`

At the observed swap prices, CES traded close to `1 USDT0`, so the realized profit is approximately **$10.5k equivalent**.

This is also the cleanest protocol-loss statement visible on-chain: the attacker exits with `10,506.125286809215449705 CES` extracted from the WhaleBit staking/oracle path.

## Evidence

### Transaction Metadata

– Transaction: `0x5d54fa839821e370b020d13a9b11b6f4f8cadc4eaed0a404ea17ad1bd725dbde`
– Chain: Polygon
– Block: `84938872`
– Timestamp: `2026-03-31T22:56:21Z`
– Status: success ( `0x1`)
– Gas used: `9,560,441`
– Log count: `234`

### Proxy Resolution

Confirmed via EIP-1967 implementation slot checks at block `84938872`:

– staking proxy `0x40465755eb5846d655bbcc8c186a477469f9ce36`-> `0x9153e149b0d90dea634ed9f7df6ff71c2109b654`
– price-module proxy `0xb5ea1d17f3d8da34a6d6a1d2acc2a148e1411868`-> `0x0729ea061132bcd76f420f9139af8957b41b90cb`
– accounting proxy `0x1caefc860308b58d0b5bb643d75c807c6a9d3a63`-> `0x35952dd1d135215cb22c07ae956ee02d4793023b`

### Trace-Backed Oracle Evidence

– `getPrice()`[0x98d5fdca] reads on price module proxy `0xb5ea…`: `30`(avg price, deviation guard only)
– `getActualPrice()`[0x6cc09081] reads on price module proxy `0xb5ea…`: `30`(spot price, used in calculations)
– `deposit`[0xcaddba3d] calls on price module proxy `0xb5ea…`: `15`
– `withdraw`[0xa527aed6] calls on price module proxy `0xb5ea…`: `15`
– `globalState()` reads on oracle pool `0xd3a933…`: `207`(forwarded from DEX adapter via `getActualPrice`/ `getAveragePrice`)
– oracle-pool `swap(…)` calls: `6` total ( `3` dumps + `3` unwinds)

### Key Transfer Evidence

– Flash loan in: `0x296b95dd0e8b726c4e358b0683ff0b6d675c35e9 -> 0xb5a8…` `51,024.905390945780848543 CES`
– Final repayment: `0xb5a8… -> 0x296b95dd0e8b726c4e358b0683ff0b6d675c35e9` `51,177.980107118618191089 CES`
– Total IGT minted: `97,366.64127003649 IGT`
– Total IGT burned: `97,366.64127003649 IGT`
– Final executor net change: `+10,506.125286809215449705 CES`

### Confidence / Limitations

High confidence on:

– transaction metadata
– flash-loan amounts
– helper counts and sequencing
– proxy resolution
– oracle-pool reads and swap order
– final profit figure

Medium confidence on:

– exact internal WhaleBit staking and accounting contract layout (unverified, trace-reconstructed only)

The price-module implementation ( `0x0729ea061132bcd76f420f9139af8957b41b90cb`) was fully decompiled by Dedaub; its code-level claims are high-confidence. The staking proxy implementation ( `0x9153…`) and accounting proxy implementation ( `0x35952…`) remain unverified; their code-level statements are trace-backed approximations.

## Related URLs

– PolygonScan TX: https://polygonscan.com/tx/0x5d54fa839821e370b020d13a9b11b6f4f8cadc4eaed0a404ea17ad1bd725dbde
– Attacker EOA: https://polygonscan.com/address/0xe66b37de57b65691b9f4ac48de2c2b7be53c5c6f
– Exploit executor: https://polygonscan.com/address/0xb5a8d7a37d60aa662f4dc9b3ef4c32a3fe21fadf
– Staking proxy: https://polygonscan.com/address/0x40465755eb5846d655bbcc8c186a477469f9ce36
– Price-module proxy: https://polygonscan.com/address/0xb5ea1d17f3d8da34a6d6a1d2acc2a148e1411868
– Oracle pool: https://polygonscan.com/address/0xd3a9331a654444f9fe7ddbaec6678c2dc9113197
– Flash pool: https://polygonscan.com/address/0x296b95dd0e8b726c4e358b0683ff0b6d675c35e9
– CES token: https://polygonscan.com/address/0x1bdf71ede1a4777db1eebe7232bcda20d6fc1610
– IGT token: https://polygonscan.com/address/0xc9e596b8b8aaf454a80a5bde0311e4d38a3690ac