Nimbus Spot-Oracle Reward Drain
Exploit Transactions
Victim Addresses
0x3aa2b9de4ce397d93e11699c3f07b769b210bbd5BSC0x706065716569f20971f9cf8c66d092824c284584BSC0xdef57a7722d4411726ff40700eb7b6876bee7ecbBSCLoss Breakdown
Similar Incidents
SellToken Reward Oracle Manipulation
39%EGD Finance Reward Oracle Manipulation
38%QiQi Reward Quote Override Drain
36%Sareon Reward Drain
35%APC Proxy Spot-Price Exploit
35%LAYER3 Oracle-Mint Drain
35%Root Cause Analysis
Nimbus Spot-Oracle Reward Drain
1. Incident Overview TL;DR
Nimbus reward contracts at 0x3aa2b9de4ce397d93e11699c3f07b769b210bbd5, 0x706065716569f20971f9cf8c66d092824c284584, and 0xdef57a7722d4411726ff40700eb7b6876bee7ecb priced GNIMB and NIMB through PriceFeedSwap contracts instead of a manipulation-resistant oracle. In setup transaction 0x7d2d8d2cda2d81529e0e0af90c4bfb39b6e74fa363c60b031d719dd9d153b012, the adversary bought GNIMB and then staked into the three pools while the stake-time conversion path read manipulated spot prices. In exploit transaction 0x42f56d3e86fb47e1edffa59222b33b73e7407d4b5bb05e23b83cb1771790f6c1, the adversary borrowed public flash liquidity, pushed the NIMB spot price, harvested rewards, withdrew stake, and exited through Nimbus liquidity.
The root cause is deterministic: PriceFeedSwap.latestAnswer() returns NimbusRouter.getAmountsOut(...) output, and the reward contracts trust that value in both stake-time and claim-time accounting. Because the adversary controls AMM reserves through public swaps, it can inflate the stored NIMB-equivalent principal and the later GNIMB payout rate inside the same economic system.
2. Key Background
Nimbus uses a PriceFeeds aggregator at 0xb8ac7fabff0d901878c269330b32cdd8d2ba3b8c to convert between tokens. For this incident, the relevant registered feeds are 0x58edbb48887711e179e66c4e86351fd06af52a87 for GNIMB and 0x199600cb7fdb1a72fc978f435e04b1cd1d260aa3 for NIMB. Both feeds are PriceFeedSwap contracts wired to the public Nimbus router at 0x2c6cf65f3cd32a9be1822855abf2321f6f8f6b24.
The three victim contracts all stake GNIMB, accrue rewards in NIMB-equivalent units, and pay out in GNIMB. When usePriceFeeds is enabled, stake-time principal conversion and claim-time payout conversion both flow through the same PriceFeeds.queryRate() path. That design matters because Nimbus router quotes are ordinary AMM spot quotes, so they are directly sensitive to adversary-controlled swaps.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-class ACT issue caused by reward accounting that treats live AMM spot quotes as trustworthy oracle prices. The reward contracts snapshot rewardsTokenAmount during _stake by converting GNIMB into NIMB-equivalent units. Later, earnedByNonce() converts accrued NIMB-equivalent rewards back into GNIMB using the same price-feed system. PriceFeeds._queryRateCall() simply forwards to the registered feed’s latestAnswer(), and each relevant PriceFeedSwap.latestAnswer() is a direct swapRouter.getAmountsOut(...) read over the GNIMB or NIMB path. The invariant that reward principal and payout must be derived from manipulation-resistant prices is therefore broken at the first latestAnswer() call. Once the adversary can move the underlying AMM reserves before staking and again before claiming, the contracts deterministically over-credit stake basis and overpay GNIMB rewards.
4. Detailed Root Cause Analysis
The reward-accounting path is explicit in the collected source:
function earnedByNonce(address account, uint256 nonce) public view returns (uint256) {
uint256 amount = stakeNonceInfos[account][nonce].rewardsTokenAmount *
(block.timestamp - stakeNonceInfos[account][nonce].stakeTime) *
stakeNonceInfos[account][nonce].rewardRate / (100 * rewardDuration);
return getTokenAmountForToken(address(rewardsToken), address(rewardsPaymentToken), amount);
}
function _stake(uint256 amount, address user) private whenNotPaused {
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 amountRewardEquivalent = getEquivalentAmount(amount);
...
stakeNonceInfos[user][stakeNonce].rewardsTokenAmount = amountRewardEquivalent;
}
The pricing layer the contracts trust is equally direct:
function _queryRateCall(address token) internal view returns (uint256 rate) {
IPriceFeedsExt _Feed = pricesFeeds[token];
rate = uint256(_Feed.latestAnswer());
}
function latestAnswer() external override view returns (uint256) {
return swapRouter.getAmountsOut(10 ** decimals, swapPath)[swapPath.length - 1]
* multiplier / MULTIPLIER_DEFAULT;
}
The setup trace shows the first leg of exploitation. Transaction 0x7d2d8d2cda2d81529e0e0af90c4bfb39b6e74fa363c60b031d719dd9d153b012 buys GNIMB through the Nimbus router and then calls stake() on all three pools. Inside those stake calls, the trace records PriceFeeds::queryRate(...) followed by PriceFeedSwap::latestAnswer() calls for both feeds, confirming that stake-time basis capture depends on manipulated spot quotes.
The exploit trace shows the second leg. Transaction 0x42f56d3e86fb47e1edffa59222b33b73e7407d4b5bb05e23b83cb1771790f6c1 borrows flash liquidity, swaps into NIMB to move the NIMB spot price, and then calls getReward() and withdraw(0). The trace again records queryRate and latestAnswer() during reward payout, so claim-time GNIMB conversion also depends on adversary-controlled AMM reserves. This is the exact code-level breakpoint described in the validated root cause.
5. Adversary Flow Analysis
The adversary cluster centers on EOA 0x86aa1c46f2ae35ba1b228dc69fb726813d95b597, which deployed and configured setup and exploit helper contracts in earlier related transactions, including deployment transaction 0x843b7769eefae98803074812429f4197f9603d7e68874ae1442fade6b3e01aab.
The on-chain flow is:
- Deploy helper contracts for the three reward pools and wire them to attacker-controlled setup and exploit contracts.
- In
0x7d2d8d2c..., buy GNIMB through the public router and stake into the three pools while GNIMB spot pricing is elevated. - Wait for accrual.
- In
0x42f56d3e..., source public flash liquidity, buy NIMB to elevate the claim-time conversion path, then callgetReward()on all three pools andwithdraw(0)on the unlocked pool. - Exit by selling extracted GNIMB back through public Nimbus liquidity.
Representative trace points from the collected evidence:
... NimbusRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(... [NBU_WBNB, GNIMB] ...)
... 0x3aA2B9de4ce397d93E11699C3f07B769b210bBD5::stake(154608210771026836098104)
... PriceFeeds::queryRate(GNIMB, NIMB)
... PriceFeedSwap::latestAnswer()
... NimbusRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(... [NBU_WBNB, NIMB] ...)
... 0x3aA2B9de4ce397d93E11699C3f07B769b210bBD5::getReward()
... 0x3aA2B9de4ce397d93E11699C3f07B769b210bBD5::withdraw(0)
... LockStakingRewardFixedAPY::getReward()
6. Impact & Losses
The combined victim loss is GNIMB only. The validated loss artifact records a total depletion of 16,480,822.532403475022448462 GNIMB, encoded on-chain as "16480822532403475022448462".
The exploit balance diff for 0x42f56d3e... shows the three reward pools going to zero GNIMB balance in the same transaction:
[
{
"holder": "0x3aa2b9de4ce397d93e11699c3f07b769b210bbd5",
"delta": "-13974836647503669949252311"
},
{
"holder": "0x706065716569f20971f9cf8c66d092824c284584",
"delta": "-1722821572931921197708161"
},
{
"holder": "0xdef57a7722d4411726ff40700eb7b6876bee7ecb",
"delta": "-783134358825383610974990"
}
]
Those deltas sum to the total reported loss and match the exploit predicate in the ACT package: the reward pools are drained because reward accounting trusted attacker-manipulable spot pricing at both stake and claim time.
7. References
- Setup transaction trace:
0x7d2d8d2cda2d81529e0e0af90c4bfb39b6e74fa363c60b031d719dd9d153b012 - Exploit transaction trace:
0x42f56d3e86fb47e1edffa59222b33b73e7407d4b5bb05e23b83cb1771790f6c1 - Victim source:
StakingRewardsFixedAPY.solandLockStakingRewardFixedAPY.sol - Price aggregator source:
PriceFeedsat0xb8ac7fabff0d901878c269330b32cdd8d2ba3b8c - Feed source:
PriceFeedSwapat0x58edbb48887711e179e66c4e86351fd06af52a87and0x199600cb7fdb1a72fc978f435e04b1cd1d260aa3 - Router source:
NimbusRouterat0x2c6cf65f3cd32a9be1822855abf2321f6f8f6b24 - Loss evidence: exploit
balance_diff.jsonfor0x42f56d3e86fb47e1edffa59222b33b73e7407d4b5bb05e23b83cb1771790f6c1