SellToken Reward Oracle Manipulation
Exploit Transactions
0xe968e648b2353cea06fc3da39714fb964b9354a1ee05750a3c5cc118da23444bVictim Addresses
0x84be9475051a08ee5364fba44de7fe83a5ecc4f1BSCLoss Breakdown
Similar Incidents
SellToken Short Oracle Manipulation
55%SellToken Arbitrary-Pair LP Drain
44%NovaX TokenStake Oracle Manipulation Exploit
39%LAYER3 Oracle-Mint Drain
38%QiQi Reward Quote Override Drain
36%TRUST/FCN Flash-Swap Reward Exploit
35%Root Cause Analysis
SellToken Reward Oracle Manipulation
1. Incident Overview TL;DR
On BNB Chain block 29005755, transaction 0xe968e648b2353cea06fc3da39714fb964b9354a1ee05750a3c5cc118da23444b exploited the verified SellToken miner contract at 0x84Be9475051a08ee5364fBA44De7FE83a5eCC4f1. The adversary used public DODO WBNB flash liquidity, manipulated the SellToken spot price used by the miner, claimed miner-held SellToken through attacker-controlled callers, and sold the extracted inventory back into the public SellToken/WBNB pool. The attack proxy 0x2cc392c0207d080aec0befe5272659d3bb8a7052 went from 0 to 446642583757970629340 raw WBNB units across the block, while the sender EOA paid 4591803700000000 wei of native gas.
The root cause is a protocol accounting flaw inside sendMiner() and Resupply(). Instead of using internal accounting or a manipulation-resistant oracle, the miner computes rewards from PancakeRouter.getAmountsOut on live AMM reserves. That let the attacker transiently skew the SellToken/USDT pool, turn a tiny matured miner position into a multi-trillion-unit SellToken payout, and withdraw SellToken directly from miner inventory.
2. Key Background
The victim contract is a verified miner contract that records per-user mining positions through setBNB(token, token1). For each position it stores a daybnb value equal to msg.value / 100, the paired quote asset, the last update time, and the accumulated mined SellToken.
For SellToken positions paired with USDT, reward settlement depends on Pancake spot pricing. The contract asks the Pancake router for the current output amount along the route WBNB -> USDT -> SellToken, then multiplies that quoted SellToken amount by the number of matured days. The payout is taken from the miner contract's own SellToken inventory, not minted from separate accounting.
The relevant code path is:
function sendMiner(address token) public {
uint[] memory vid = MyminerID[_msgSender()][token];
address token1 = selladdress[token][vid[0]].pair;
...
uint _day = (block.timestamp - selladdress[token][vid[i]].time) / DAYSTIME;
uint agk = getbnb(token, token1, selladdress[token][vid[i]].daybnb) * _day;
if (IERC20(token).balanceOf(_msgSender()) >= agk) {
IERC20(token).transfer(_msgSender(), agk);
selladdress[token][vid[i]].sumAGK += agk;
}
}
function getbnb(address _tolens, address bnbOrUsdt, uint bnb) public view returns (uint) {
...
routePath[0] = _WBNB;
routePath[1] = _USDT;
routePath[2] = _tolens;
return IRouter(_router).getAmountsOut(bnb, routePath)[2];
}
Resupply() reuses the same reward calculation. The only gate before payout is IERC20(token).balanceOf(_msgSender()) >= agk, which is satisfied if the caller can temporarily hold enough SellToken during the same transaction.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-class ACT opportunity, not a privileged-access issue. The miner contract trusts a flash-manipulable Pancake spot quote as reward accounting, so the payout amount is fully attacker-controlled within one transaction. The contract then transfers the quoted SellToken amount from its own balance to the caller. Because the claim path checks only whether the caller temporarily holds at least agk SellToken, flash-borrowed SellToken is enough to satisfy the gate. Public flash liquidity, public Pancake liquidity, and the public setBNB and sendMiner interfaces are sufficient to realize the exploit without any privileged credential. The core invariant break is that a matured position should release a reward derived from stable protocol accounting, but the miner instead lets transient AMM reserve changes directly determine how much inventory it gives away.
4. Detailed Root Cause Analysis
The exploit starts from a public pre-state at block 29005754: the miner still held substantial SellToken inventory, the SellToken/WBNB and SellToken/USDT Pancake pairs were live, and the attacker could create matured positions through setBNB(). In the seed transaction the attacker borrowed WBNB from three public DODO pools, totaling 769005289801718964671 wei.
The attacker then spent 630000000000000000000 wei of WBNB on the SellToken/WBNB pair and immediately dumped the purchased 3079029717807410649060316 raw SellToken units into the SellToken/USDT pair. That left the SellToken/USDT pair with only 9076658 raw USDT units and 3079029768246293031238405 raw SellToken units, which heavily distorted the router quote used by the miner.
The manipulated quote is visible directly in the seed trace:
PancakeRouter::getAmountsOut(
9000000000000,
[WBNB, USDT, SellToken]
)
<- [9000000000000, 2116986751016792, 3079029755011754173992263]
That quoted 3079029755011754173992263 raw SellToken units for a single matured position whose daybnb was only 9000000000000. The attacker then flash-swapped 3174556092237876693762871 raw SellToken units from the SellToken/WBNB pair so the active caller would satisfy the miner's balanceOf(msg.sender) >= agk gate during payout.
The trace shows four attacker-controlled helper addresses invoking sendMiner() and receiving direct transfers from the miner:
0x84Be9475...::sendMiner(SellToken)
PancakeRouter::getAmountsOut(9000000000000, [WBNB, USDT, SellToken])
<- [9000000000000, 2116986751016792, 3079029755011754173992263]
SellToken::transfer(0x4FD94EbF..., 3079029755011754173992263)
0x84Be9475...::sendMiner(SellToken)
SellToken::transfer(0xF431623A..., 3079029755011754173992263)
0x84Be9475...::sendMiner(SellToken)
SellToken::transfer(0xFe36BD64..., 3079029755011754173992263)
0x84Be9475...::sendMiner(SellToken)
SellToken::transfer(0xdC6053c3..., 3079029755011754173992263)
After those claims, the attacker collected helper-held LP tokens, burned SellToken/USDT liquidity, converted the residual USDT back into SellToken, sold 15382401367447885453972142 raw SellToken units into the SellToken/WBNB pair, repaid the DODO flash liquidity, and kept the remaining WBNB.
The balance diff and post-state evidence confirm the economic result:
{
"miner_sell_delta": "-12316119020047016695969052",
"attack_proxy_wbnb_before": "0",
"attack_proxy_wbnb_after": "446642583757970629340",
"sender_native_gas_paid": "4591803700000000"
}
The miner therefore lost 12316119020047016695969052 raw SellToken units because reward accounting trusted attacker-controlled spot reserves. The flash loans only supplied capital and temporary inventory for the gate; they are not the root cause.
5. Adversary Flow Analysis
The same EOA 0x0060129430df7ea188be3d8818404a2d40896089 deployed the attack proxy in transaction 0xede5176e2743eab19a35237ca7438c7dbb4874e87956b105ccec058fadb91c7b and later submitted the seed exploit transaction. During the exploit, execution flowed through proxy 0x2cc392c0207d080aec0befe5272659d3bb8a7052, implementation 0x8991A6FA4ee05d67520834C4Ec677740E3e8f588, four helper contracts 0x4fd94ebf7b5e5f92e138750a95aec512e2b72735, 0xf431623a1a31fca4b23488a4522bf0bd9d09dbac, 0xfe36bd64e7602e8fff768fc2e556363d7fd65e06, and 0xdc6053c3ced24f1ae7f7ffb04a2cfd300f40e95d, plus shared helper implementation 0xF6f1439E29E23c045D24544007774ab645A4d05e.
The on-chain execution sequence was:
- Borrow WBNB from three public DODO pools.
- Swap
630WBNB into SellToken on the SellToken/WBNB pair. - Dump the purchased SellToken into the SellToken/USDT pair to collapse the USDT reserve and inflate the miner's SellToken quote.
- Flash-swap SellToken from the SellToken/WBNB pair so the caller-side balance gate inside
sendMiner()passes. - Call
sendMiner()four times through helper contracts, each of which receives SellToken directly from the miner and returns value to the proxy. - Remove SellToken/USDT liquidity, convert leftover USDT back into SellToken, then sell the accumulated SellToken into the SellToken/WBNB pair.
- Repay the flash liquidity and retain WBNB profit on the proxy.
Every external dependency in that sequence is public and permissionless. The attacker did not need any privileged role on the miner, the Pancake pools, or the DODO pools.
6. Impact & Losses
The direct protocol loss in the seed transaction was:
SELLC:12316119020047016695969052raw units (decimal = 18)
The miner contract at 0x84Be9475051a08ee5364fBA44De7FE83a5eCC4f1 lost that SellToken inventory to the attacker cluster. The realized profit predicate is also satisfied: the attack proxy held 446642583757970629340 raw WBNB units after the exploit transaction, compared with 0 before the block, while the sender EOA paid only 4591803700000000 wei in gas.
7. References
- Seed exploit transaction:
0xe968e648b2353cea06fc3da39714fb964b9354a1ee05750a3c5cc118da23444b - Related proxy deployment transaction:
0xede5176e2743eab19a35237ca7438c7dbb4874e87956b105ccec058fadb91c7b - Victim contract:
0x84Be9475051a08ee5364fBA44De7FE83a5eCC4f1 - SellToken:
0xa645995e9801F2ca6e2361eDF4c2A138362BADe4 - SellToken/WBNB pair:
0x358EfC593134f99833C66894cCeCD41F550051b6 - SellToken/USDT pair:
0x9523B023E1D2C490c65D26fad3691b024d0305D7 - Seed metadata artifact:
/workspace/session/artifacts/collector/seed/56/0xe968e648b2353cea06fc3da39714fb964b9354a1ee05750a3c5cc118da23444b/metadata.json - Seed trace artifact:
/workspace/session/artifacts/collector/seed/56/0xe968e648b2353cea06fc3da39714fb964b9354a1ee05750a3c5cc118da23444b/trace.cast.log - Seed balance diff artifact:
/workspace/session/artifacts/collector/seed/56/0xe968e648b2353cea06fc3da39714fb964b9354a1ee05750a3c5cc118da23444b/balance_diff.json - Observed WBNB balance artifact:
/workspace/session/artifacts/auditor/iter_0/observed_balances.json - Verified miner source:
https://bscscan.com/address/0x84be9475051a08ee5364fba44de7fe83a5ecc4f1#code