Calculated from recorded token losses using historical USD prices at the incident time.
0xa487da310b02dfa3e6da2d9b3c797b656957924f4b08e38ed256cfeed48dbbca0x2001144a0485b0b3748a167848cdd73837345d73BSCTransaction 0xa487da310b02dfa3e6da2d9b3c797b656957924f4b08e38ed256cfeed48dbbca on BSC block 84377206 is an ACT exploit against InugamiStaking (0x2001144a0485b0b3748a167848cdd73837345d73).
A fresh, unprivileged EOA (0x43c2f458e3aa73dcfd872144ded5c7faf56e33f8) deployed an executor contract (0x20a8047fd8b23db7446041c32c442a77eb46f989) and, in one transaction, acquired LP, staked LP, reactivated an expired reward pool with a 1 wei WBNB top-up, claimed historical rewards, then exited and transferred profit back to the EOA.
Root cause: expired reward pools bypass user debt initialization in _updateUserDebt, while streamReward() can reactivate those pools without resetting stale accRewardPerShare. This allows a newly staked user to claim rewards accrued before stake time.
The staking system uses accumulator-based reward accounting:
accRewardPerShare tracks cumulative reward per stake unit._userDebt) must be initialized against current accumulator at entry time.Security-critical expectation: after stake, user debt for each reward PID should be set to balance * accRewardPerShare / 1e36, so claims only include post-entry accrual.
In this incident, reward PID0 (WBNB) was expired but retained a very large stale accumulator and significant reserves. That state made debt initialization gating exploitable.
This is an ATTACK-class logic/accounting exploit, not a privileged-access incident. The vulnerable invariant is debt initialization consistency at user entry across all active accounting dimensions.
The verified InugamiStaking source confirms _updateUserDebt only updates debt when endRewardTimestamp >= block.timestamp. For expired pools, debt is left unchanged (zero for a new staker). streamReward() is permissionless and can reactivate an expired pool by adding any positive reward amount, while preserving stale accumulator context. After reactivation, claim uses current balance and stale high accumulator against zero debt, producing an oversized payout. Trace evidence confirms the exact sequence stake -> transfer(1 wei WBNB) -> streamReward -> claim -> unstake.
Victim code (verified source for 0x2001144a0485b0b3748a167848cdd73837345d73) shows both enabling conditions:
function streamReward() external {
_updatePools(false, address(0));
...
if (newRewards > 0) {
...
rewardInfo.reserves += newRewards;
rewardInfo.endRewardTimestamp = lastRewardTimestamp + WINDOW_LENGTH;
}
}
function _updateUserDebt(address user) internal {
for (uint256 i = 0; i < rewardTokensCount; ++i) {
if (
rewardInfos[i].rewardToken != address(0) &&
rewardInfos[i].endRewardTimestamp >= block.timestamp
) {
_userDebt[i][user] =
(balanceOf[user] * rewardInfos[i].accRewardPerShare) / 1e36;
}
}
}
If a reward PID is expired when stake executes, debt is not initialized for that PID.
State snapshot at block 0x5077e75 (pre-state) shows PID0 expired but still carrying large stale accounting state and reserves:
reward0 token: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
accRewardPerShare: 911232950638852218698941779818023669046
reserves: 13904954155454625823
endRewardTimestamp: 1649459132 (expired)
WBNB bal: 13904954155454625823
Collector trace confirms the deterministic sequence and key values:
0x2001144a...::stake(15259494452769803)
WBNB::transfer(0x2001144a..., 1)
0x2001144a...::streamReward()
0x2001144a...::claim(0x20A8047f..., [0])
WBNB::transfer(0x20A8047f..., 13904954155454625145)
0x2001144a...::unstake(15259494452769803)
Post-state at block 0x5077e76 shows pool reactivated (endRewardTimestamp pushed forward) and WBNB reserve nearly drained:
reward0 reserves: 679
reward0 endRewardTimestamp: 1773120610
WBNB bal: 679
Intended invariant: for each reward PID, a new staker should have debt initialized to current accumulator-weighted balance at stake time.
Observed breakpoint: because PID0 was expired at stake time, debt remained zero; after reactivation, claim was computed against stale high accumulator and paid out historical rewards, violating temporal reward attribution.
0x43c2f458e3aa73dcfd872144ded5c7faf56e33f80x20a8047fd8b23db7446041c32c442a77eb46f9890xa487da31...) funds and executes the attack path atomically.0xe7989a82615b68c09b6fc0d1d24c95551a47e0cf).InugamiStaking.1 wei WBNB to staking.streamReward() to reactivate expired PID0.claim(...,[0]) to receive 13904954155454625145 wei WBNB.All calls are public/permissionless and feasible by any unprivileged actor.
Victim-side loss:
13.904954155454625144 WBNB (13904954155454625144 wei), leaving 679 wei.Attacker-side realized gain:
+13895178308441569254 wei (+13.895178308441569254 BNB) in the exploit transaction.Affected victim contract:
InugamiStaking at 0x2001144a0485b0b3748a167848cdd73837345d73.0xa487da310b02dfa3e6da2d9b3c797b656957924f4b08e38ed256cfeed48dbbca (BSC, block 84377206).0x2001144a0485b0b3748a167848cdd73837345d73.0x43c2f458e3aa73dcfd872144ded5c7faf56e33f8.0x20a8047fd8b23db7446041c32c442a77eb46f989.artifacts/collector/seed/56/0xa487da310b02dfa3e6da2d9b3c797b656957924f4b08e38ed256cfeed48dbbca/trace.cast.logartifacts/collector/seed/56/0xa487da310b02dfa3e6da2d9b3c797b656957924f4b08e38ed256cfeed48dbbca/balance_diff.jsonartifacts/auditor/iter_0/staking_state_pre_post.txt