NovaX TokenStake Oracle Manipulation Exploit
Exploit Transactions
0xb1ad1188d620746e2e64785307a7aacf2e8dbda4a33061a4f2fbc9721048e012Victim Addresses
0x55c9eebd368873494c7d06a4900e8f5674b11bd2BSCLoss Breakdown
Similar Incidents
SellToken Short Oracle Manipulation
38%VistaFinance oracle mispricing enables VISTA flash-loan arbitrage drain
35%FortuneWheel BNBP/Link Price-Manipulation Fee Drain
33%0x3Af7 burn-rate manipulation drains WBNB from BSC pool
33%SOF Sell-Hook Reserve Manipulation Drains PancakeSwap V2 USDT Liquidity
33%SmartBank balance-manipulation bug drains USDT via flash-loan roundtrip
32%Root Cause Analysis
NovaX TokenStake Oracle Manipulation Exploit
1. Incident Overview TL;DR
On BNB Smart Chain (chainid 56), a single adversary-crafted transaction in block 41116211 (tx 0xb1ad1188d6…e012) exploited the NovaX TokenStake contract by abusing its price oracle. A helper contract first manipulated the NovaX/USDT PancakeSwap pair that feeds Oracle 0xaeb77ff2…, then performed a same-transaction stake followed by withdraw against TokenStake 0x55c9eebd…. Because TokenStake directly uses the manipulated oracle output for both recording stake value and redeeming it, the attacker withdrew more NovaX than deposited and simultaneously drained USDT from the NovaX/USDT pool. As a result, TokenStake lost 84,319,204,255,820,354,933,135 NovaX tokens and the NovaX/USDT pair lost 26,476,325,961,551,338,835,137 USDT, while adversary EOAs received 66,251,109,303,267,001,599,526 NovaX and 24,971.812420929473237347 USDT. The gas cost (0.00693603 BNB) is negligible relative to the USDT profit, so the transaction is strictly profitable and constitutes an anyone-can-take (ACT) opportunity.
2. Key Background
The NovaX ecosystem on BSC includes:
- NovaXToken (
0xb800aff8…) — an ERC‑20-like token with standard balances and taxes. - TokenStake (
0x55c9eebd…) — a staking contract that accepts NovaX deposits into several pools and tracks each stake’s value in an internal “USD” unit. - Oracle (
0xaeb77ff2…) — a price oracle that converts between USD‑denominated values and NovaX amounts using a combination of an internal swap contract and a NovaX/USDT PancakeSwap pair. - InternalSwap (
0xe7610919…) — a contract recording an owner‑set NovaX/USDT price used by the oracle as one of its sources. - PancakeRouter (
0x10ed43c7…) and Pancake pairs (0x05a911cc…NovaX/USDT,0x7efaef62…USDT/BUSD) — standard AMM infrastructure used for price manipulation.
TokenStake values stakes and withdrawals by calling the oracle. For a given token token and amount _tokenAmount, it records a USD‑denominated value using:
function tokenToUsd(address token, uint256 _tokenAmount) public view returns (uint256) {
address oracleContract = oracleContracts[token];
return (1000000 * _tokenAmount)
/ IOracle(oracleContract).convertUsdBalanceDecimalToTokenDecimal(1000000);
}
function usdToToken(address token, uint256 _usdAmount) public view returns (uint256) {
address oracleContract = oracleContracts[token];
return IOracle(oracleContract).convertUsdBalanceDecimalToTokenDecimal(_usdAmount);
}
Each stake stores totalValueStakeUsd = tokenToUsd(novaxToken, amount). Later, withdraw(_stakeId) computes the number of NovaX tokens to return by inverting this via usdToToken, again delegating to the oracle.
The Oracle contract combines an internal swap price and, when configured with typeConvert = 2, a NovaX/USDT Pancake pair price. The critical function is:
function convertUsdBalanceDecimalToTokenDecimal(uint256 _balanceUsdDecimal)
public
view
returns (uint256)
{
uint256 tokenInternalSwap = convertInternalSwap(_balanceUsdDecimal, true);
uint256 tokenPairConvert;
if (pairAddress != address(0)) {
(uint256 _reserve0, uint256 _reserve1, ) = IPancakePair(pairAddress).getReserves();
(uint256 _tokenBalance, uint256 _stableBalance) =
address(tokenAddress) < address(stableToken)
? (_reserve0, _reserve1)
: (_reserve1, _reserve0);
uint256 _minTokenAmount = (_balanceUsdDecimal * minTokenAmount) / 1000000;
uint256 _maxTokenAmount = (_balanceUsdDecimal * maxTokenAmount) / 1000000;
uint256 _tokenAmount = (_balanceUsdDecimal * _tokenBalance) / _stableBalance;
if (_tokenAmount < _minTokenAmount) {
tokenPairConvert = _minTokenAmount;
}
if (_tokenAmount > _maxTokenAmount) {
tokenPairConvert = _maxTokenAmount;
}
tokenPairConvert = _tokenAmount; // overwrites any min/max clamp
}
if (typeConvert == 1) {
return tokenInternalSwap;
} else if (typeConvert == 2) {
return tokenPairConvert;
} else {
if (tokenPairConvert == 0 || tokenInternalSwap == 0) {
return tokenPairConvert + tokenInternalSwap;
} else {
return (tokenPairConvert + tokenInternalSwap) / 2;
}
}
}
Because the final assignment tokenPairConvert = _tokenAmount overwrites any clamping, the oracle’s pair‑based output can be pushed arbitrarily high or low within a single transaction by manipulating the NovaX/USDT reserves.
3. Vulnerability Analysis & Root Cause Summary
The root cause is a price‑oracle logic bug combined with same‑transaction stake and withdraw in TokenStake:
- TokenStake encodes each stake’s USD value using the oracle’s NovaX/USDT price at deposit time, and later uses the oracle again at withdraw time to convert that USD value back into NovaX.
- Oracle’s
convertUsdBalanceDecimalToTokenDecimalintends to clamp the pair‑derived token amount betweenminTokenAmountandmaxTokenAmount, but then unconditionally overwrites the clamped value with the raw_tokenAmountcomputed from AMM reserves. - When Oracle is configured with
typeConvert = 2(pair‑only), TokenStake’stokenToUsd/usdToTokeneffectively compute:stakeValueUsd = 1e6 * A / k_depositwithdrawTokenValue = stakeValueUsd * k_withdraw / 1e6 = A * (k_withdraw / k_deposit)wherek_*is the oracle’s tokens‑per‑USD output based on pair reserves.
- A helper contract can manipulate the NovaX/USDT pair between the “deposit” and “withdraw” steps of a single transaction so that
k_withdraw / k_deposit > 1, causing TokenStake to return more NovaX than was staked.
In short, TokenStake trusts an unbounded, manipulation‑prone spot price from a PancakeSwap pair as a stable conversion factor between stake and withdraw, enabling an over‑withdrawal of NovaX in a single transaction.
4. Detailed Root Cause Analysis
4.1 Stake and Withdraw Accounting
When a user stakes NovaX via TokenStake, the contract:
- Looks up the stake pool to obtain
stakeToken(NovaX) and its parameters. - Calls
tokenToUsd(stakeToken, _stakeValue)to compute a USD‑denominated amount. - Stores both the raw token amount and the derived USD value in the
StakedTokenstruct.
The relevant functions and withdraw logic are:
function tokenToUsd(address token, uint256 _tokenAmount) public view returns (uint256) {
address oracleContract = oracleContracts[token];
return (1000000 * _tokenAmount)
/ IOracle(oracleContract).convertUsdBalanceDecimalToTokenDecimal(1000000);
}
function usdToToken(address token, uint256 _usdAmount) public view returns (uint256) {
address oracleContract = oracleContracts[token];
return IOracle(oracleContract).convertUsdBalanceDecimalToTokenDecimal(_usdAmount);
}
function withdraw(uint256 _stakeId) public override lock {
StakedToken memory _stakedUserToken = stakedToken[_stakeId];
require(_stakedUserToken.userAddress == msg.sender, "TS:O");
require(!_stakedUserToken.isWithdraw, "TS:W");
require(canNotWithdraw[_stakeId] == 0, "TS:C");
require(_stakedUserToken.unlockTime <= block.timestamp, "TS:T");
claimInternal(_stakeId);
StakeTokenPools memory stakeTokenPool = stakeTokenPools[_stakedUserToken.poolId];
address stakeToken = stakeTokenPool.stakeToken;
uint256 withdrawTokenValue =
usdToToken(stakeToken, _stakedUserToken.totalValueStakeUsd);
require(IERC20(stakeToken).balanceOf(address(this)) >= withdrawTokenValue, "TS:E");
require(IERC20(stakeToken).transfer(_stakedUserToken.userAddress, withdrawTokenValue), "TS:U");
// … update accounting …
}
Let A be the number of NovaX tokens staked. Define k(P) as the oracle’s NovaX‑per‑USD output at price state P. At stake time:
- TokenStake calls
convertUsdBalanceDecimalToTokenDecimal(1,000,000)using the current pair reserves, obtainingk_deposit. - It stores
stakeValueUsd = 1,000,000 * A / k_deposit.
At withdraw time:
- It calls the oracle again with
_balanceUsdDecimal = stakeValueUsd, using the (possibly manipulated) current reserves to getk_withdraw. - It returns
withdrawTokenValue = stakeValueUsd * k_withdraw / 1,000,000 = A * (k_withdraw / k_deposit).
If the adversary can increase k_withdraw relative to k_deposit within the same transaction, withdrawTokenValue strictly exceeds A, violating the intended invariant:
For each
stakeId,withdraw(_stakeId)must not return more NovaX (valued via a trusted, bounded price source) than corresponds to the stake’s recorded USD value based on the deposit‑time price.
4.2 Oracle Min/Max Clamp Bug
Oracle attempts to guard against extreme prices by defining minTokenAmount and maxTokenAmount and computing bounds on the token amount for a given USD value. However, the implementation:
- Computes
_minTokenAmount,_maxTokenAmount, and_tokenAmountfrom NovaX/USDT reserves. - Conditionally sets
tokenPairConvert = _minTokenAmountor_maxTokenAmountif_tokenAmountis below/above the bounds. - Unconditionally executes
tokenPairConvert = _tokenAmount.
This last assignment nullifies the min/max clamp. When typeConvert == 2, the return value is tokenPairConvert, which is always equal to the raw _tokenAmount derived from the current reserves. Thus:
- The oracle is effectively a simple spot‑price converter based solely on pair reserves.
- Any intra‑transaction manipulation of the NovaX/USDT pair directly and unboundedly affects the conversion rate used by TokenStake.
4.3 Exploit Transaction Behavior
The exploit transaction 0xb1ad1188d6…e012 has:
- Sender / gas payer: EOA
0x81ca56b6…. - To: helper contract
0x42bc5a77…. - Value: 0 BNB, gas cost 6,936,030,000,000,000 wei (0.00693603 BNB) per
native_balance_deltas.
The callTracer trace shows a sequence of USDT and NovaX movements through PancakeRouter and the NovaX/USDT pair, followed by TokenStake calls and final profit transfers. A shortened excerpt:
{
"calls": [
{
"from": "0x7efaef62…",
"to": "0x55d39832…",
"type": "CALL",
"input": "0xa9059cbb… -> transfer USDT to 0x42bc5a77…"
},
{
"from": "0x42bc5a77…",
"to": "0x55d39832…",
"type": "CALL",
"input": "0x095ea7b3… -> approve USDT for PancakeRouter"
},
{
"from": "0x10ed43c7…",
"to": "0x05a911cc…",
"type": "CALL",
"input": "0x23b872dd/0x022c0d9f… -> move USDT into NovaX/USDT pair and execute swap"
},
{
"from": "0x42bc5a77…",
"to": "0x55c9eebd…",
"type": "CALL",
"input": "0x7b0472f0… -> TokenStake.stake(poolId, amount)"
},
{
"from": "0x42bc5a77…",
"to": "0x55c9eebd…",
"type": "CALL",
"input": "0x2e1a7d4d… -> TokenStake.withdraw(stakeId)"
},
{
"from": "0x42bc5a77…",
"to": "0x55d39832…",
"type": "CALL",
"input": "0xa9059cbb… -> transfer USDT to 0x80773808…"
}
],
"from": "0x81ca56b6…",
"to": "0x42bc5a77…",
"input": "0xe235aeca… (helper entrypoint)"
}
The corresponding balance_diff.json confirms the net asset movements:
{
"native_balance_deltas": [
{
"address": "0x81ca56b6…",
"delta_wei": "-6936030000000000"
}
],
"erc20_balance_deltas": [
{
"token": "0x55d39832…", "contract_name": "BEP20USDT",
"holder": "0x05a911cc…", "delta": "-26476325961551338835137"
},
{
"token": "0x55d39832…",
"holder": "0x80773808…", "delta": "24971812420929473237347"
},
{
"token": "0xb800aff8…", "contract_name": "NovaXToken",
"holder": "0x55c9eebd…", "delta": "-84319204255820354933135"
},
{
"token": "0xb800aff8…",
"holder": "0xb45fe3a6…", "delta": "66251109303267001599526"
}
]
}
These diffs show:
- TokenStake loses 84,319,204,255,820,354,933,135 NovaX tokens.
- The NovaX/USDT pair loses 26,476,325,961,551,338,835,137 USDT.
- EOA
0xb45fe3a6…gains 66,251,109,303,267,001,599,526 NovaX. - EOA
0x80773808…gains 24,971,812,420,929,473,237,347 USDT.
Oracle and InternalSwap are only read during the transaction (no state writes), so the exploit relies purely on AMM reserve manipulation plus the oracle’s logic bug, not on misconfigured InternalSwap values during the tx.
4.4 ACT Opportunity Characterization
All steps of the exploit are available to any unprivileged on‑chain actor:
- TokenStake’s
stakeandwithdrawfunctions are publicly callable and do not require special roles. - Oracle’s configuration (
pairAddress,typeConvert = 2, min/max values) is set beforehand and used as‑is by any caller. - PancakeRouter and the NovaX/USDT pair are standard public DeFi contracts; anyone can route swaps to manipulate reserves, subject only to liquidity and slippage.
- The helper contract
0x42bc5a77…is adversary‑controlled but not privileged; any other adversary can deploy an identical contract or equivalent logic to reproduce the strategy.
The success predicate is a strictly positive profit in BEP20 USDT, measured on EOA 0x80773808…:
- Before the exploit tx: 88,921,629,982,434,813,436 USDT units.
- After the exploit tx: 25,060,734,050,911,908,050,783 USDT units.
- Net gain: 24,971,812,420,929,473,237,347 USDT units, versus a fixed gas cost of 0.00693603 BNB paid by EOA
0x81ca56b6….
This constitutes a clear, reproducible ACT opportunity: any adversary with capital to manipulate the NovaX/USDT pair and pay gas can realize the same profit mechanism.
5. Adversary Flow Analysis
5.1 Adversary‑Related Cluster Accounts
The minimal adversary‑related cluster consists of:
0x81ca56b6973ff63e3ff2b3f99cb6a6d211269e79— EOA sender of both the helper deployment tx and the exploit tx; pays gas.0x42bc5a77985b2149a8fd085bf1d3fcda4eb71d53— helper/orchestrator contract deployed by 0x81ca56…; its runtime bytecode hard‑codes TokenStake, NovaXToken, Oracle, PancakeRouter, NovaX/USDT, and profit recipient addresses.0x8077380811e1228e21cb9c157b4de54ca7cefcee— EOA recipient of USDT profit; runtime bytecode at block 41116210 is empty (result: "0x"), confirming it is an EOA.0xb45fe3a6db227a05bbe43c0786d69bf3da3bedc3— EOA recipient of NovaX profit; also has empty runtime bytecode.
5.2 Lifecycle Stages
-
Helper deployment (priming stage)
- Tx:
0x5b483b45164063672e84e8fa69f920d340e8062451aeb4f0b1741908d0256e1ain block 41116150. - Sender:
0x81ca56…deploying0x42bc5a77…. - Runtime bytecode analysis shows hard‑coded references to TokenStake, NovaXToken, Oracle, PancakeRouter, NovaX/USDT pair, USDT pair, and the USDT profit address
0x8077…. This establishes 0x42bc5a77… as an adversary‑controlled orchestrator dedicated to the exploit.
- Tx:
-
Single‑tx price manipulation and over‑withdrawal (exploit stage)
- Tx:
0xb1ad1188d620746e2e64785307a7aacf2e8dbda4a33061a4f2fbc9721048e012in block 41116211. - Steps (from callTracer and decoded ABIs):
- USDT is transferred from the external USDT/BUSD pair
0x7efaef62…to0x42bc5a77…, then approved and routed via PancakeRouter0x10ed43c7…into the NovaX/USDT pair0x05a911cc…, skewing its reserves and effective NovaX/USDT price. - With the manipulated price in place,
0x42bc5a77…callsTokenStake.stake(poolId, amount)on0x55c9eebd…, causing TokenStake to create a new stake withtotalValueStakeUsdcomputed at the “cheap” deposit‑time oracle price. - Immediately afterward in the same transaction,
0x42bc5a77…callsTokenStake.withdraw(stakeId)for the freshly created stake. TokenStake calls Oracle again, now observing a different NovaX/USDT reserve ratio, and computes a largerwithdrawTokenValue = A * (k_withdraw / k_deposit). - TokenStake transfers
withdrawTokenValueNovaX to the orchestrator/beneficiary, while the NovaX/USDT pair and TokenStake absorb the opposing legs of the manipulated swaps. - Finally,
0x42bc5a77…routes the excess USDT to EOA0x8077…and the excess NovaX to EOA0xb45f…, crystallizing profit.
- USDT is transferred from the external USDT/BUSD pair
- Tx:
No privileged roles or governance operations are used; all calls are standard external function calls available to any account.
6. Impact & Losses
From balance_diff.json for tx 0xb1ad…e012:
-
NovaX losses (victim side)
- TokenStake
0x55c9eebd…loses84,319,204,255,820,354,933,135NovaX units. - The NovaX/USDT pair
0x05a911cc…gains18,068,094,952,553,353,333,609NovaX units (part of the manipulated swap path). - EOA
0xb45fe3a6…gains66,251,109,303,267,001,599,526NovaX units as direct profit.
- TokenStake
-
USDT losses (liquidity side)
- NovaX/USDT pair
0x05a911cc…loses26,476,325,961,551,338,835,137USDT units. - External USDT/BUSD pair
0x7efaef62…gains1,504,513,540,621,865,597,790USDT units. - EOA
0x80773808…gains24,971,812,420,929,473,237,347USDT units as direct profit.
- NovaX/USDT pair
-
Gas cost
- Gas payer
0x81ca56…pays exactly6,936,030,000,000,000wei (0.00693603 BNB).
- Gas payer
Defining the reference asset as BEP20 USDT alone, the adversary’s portfolio change on 0x8077… is:
- Before: 88.921629982434813436 USDT.
- After: 25,060.734050911908050783 USDT.
- Delta: +24,971.812420929473237347 USDT.
Even after accounting for any reasonable USDT value of 0.00693603 BNB gas, the net profit in USDT is strictly positive. Additional upside from the NovaX position on 0xb45f… is not required to prove profitability.
7. References
-
Seed transaction metadata and traces
- Tx
0xb1ad1188d620746e2e64785307a7aacf2e8dbda4a33061a4f2fbc9721048e012, chainid 56 (BSC). - Metadata, receipt, and cast trace:
artifacts/root_cause/seed/56/0xb1ad…e012/metadata.json,trace.cast.log. - CallTracer and prestate traces:
artifacts/root_cause/data_collector/iter_2/tx/56/0xb1ad…e012/debug_trace_callTracer.json,
artifacts/root_cause/data_collector/iter_1/tx/56/0xb1ad…e012/debug_trace_prestate_tracer.json.
- Tx
-
Balance diffs and profit computation
artifacts/root_cause/seed/56/0xb1ad…e012/balance_diff.json(NovaX and USDT deltas, native BNB gas cost).
-
Contract source code (verified)
- NovaXToken
0xb800aff8391abacdeb0199ab9cebf63771fcf491. - TokenStake
0x55c9eebd368873494c7d06a4900e8f5674b11bd2(includestokenToUsd,usdToToken, andwithdraw). - Oracle
0xaeb77ff298970a7fb6dc6f5c4a7f02426db814ea(includesconvertUsdBalanceDecimalToTokenDecimal). - InternalSwap
0xe76109198ffcd88e16e95e4cc0bafe01f5756c6a. - PancakeRouter
0x10ed43c718714eb63d5aa57b78b54704e256024e. - NovaX/USDT pair
0x05a911cc7b9e1481a795ba548049285715a6c7bcand USDT/BUSD pair0x7efaef62fddcca950418312c6c91aef321375a00. - All under
artifacts/root_cause/data_collector/iter_1/contract/56/*/source/.
- NovaXToken
-
Helper and adversary account evidence
- Helper deployment tx and txlist traces for
0x81ca56…and0x42bc5a77…:
artifacts/root_cause/data_collector/iter_2/address/56/0x81ca56…/txlist_alt_41115000_41120000.json_traces.json. - Runtime bytecode for helper and profit EOAs:
artifacts/root_cause/data_collector/iter_2/contract/56/0x42bc5a77…/runtime_bytecode.json,
…/0x80773808…/runtime_bytecode.json,
…/0xb45fe3a6…/runtime_bytecode.json.
- Helper deployment tx and txlist traces for
-
Root cause analyzer artifacts
- Final analyzer reasoning and ACT gap analysis:
artifacts/root_cause/root_cause_analyzer/iter_3/current_analysis_result.json.
- Final analyzer reasoning and ACT gap analysis: