sDAO Reward Inflation Attack
Exploit Transactions
0xb3ac111d294ea9dedfd99349304a9606df0b572d05da8cedf47ba169d10791edVictim Addresses
0x6666625ab26131b490e7015333f97306f05bf816BSC0x333896437125ff680f146f18c8a164be831c4c71BSCLoss Breakdown
Similar Incidents
EGD Finance Reward Oracle Manipulation
38%OKC Flash-LP Reward Drain
38%TRUST/FCN Flash-Swap Reward Exploit
36%NFD Reward Sybil Exploit
36%UEarnPool Reward Drain
36%SellToken Reward Oracle Manipulation
35%Root Cause Analysis
sDAO Reward Inflation Attack
1. Incident Overview TL;DR
On BSC block 23241441, transaction 0xb3ac111d294ea9dedfd99349304a9606df0b572d05da8cedf47ba169d10791ed used a public flash loan, Pancake swaps, and sDAO staking functions to manufacture an inflated staking payout and exit with 13661918634705551920897 more USDT at the attacker helper contract. The exploit path was permissionless: an unprivileged EOA drove a helper contract that borrowed USDT, bought sDAO, created LP, staked LP, drained the staking contract’s LP balance with withdrawTeam, restored only dust LP, claimed an outsized reward through getReward, sold the reward back into USDT, and repaid the loan.
The root cause is a combination bug in sDAO. First, withdrawTeam(address) is externally callable without access control and transfers the entire balance of any token from sDAO to TEAM. Second, reward distribution uses LPInstance.balanceOf(address(this)) as the denominator in getPerTokenReward(), so draining LP from the contract collapses the denominator while userLPStakeAmount still reflects the attacker’s previously staked position.
2. Key Background
sDAO is both the project token and the LP staking contract. LP stakers accrue rewards from transfer taxes collected when sDAO moves to or from the configured LP pair. The verified contract increments totalStakeReward during taxed LP-facing transfers and later distributes newly accumulated reward through getPerTokenReward().
The key accounting split is:
userLPStakeAmount[account]tracks how much LP each staker deposited throughstakeLP.getPerTokenReward()divides newly accrued reward byLPInstance.balanceOf(address(this)), which is the contract’s live ERC-20 LP balance rather than an internal total-staked variable.
That design means external movement of LP out of the contract changes the reward denominator even if staking records are unchanged. Because Pancake LP tokens are standard ERC-20s, sDAO can transfer them away exactly like any other token balance.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ACT attack caused by broken access control plus reward-accounting drift. The verified sDAO source exposes withdrawTeam(address _token) external with no modifier, and the function transfers the entire _token balance from sDAO to TEAM. The same contract computes reward-per-token with (totalStakeReward - lastTotalStakeReward) * 1e18 / LPInstance.balanceOf(address(this)), so the reward denominator is the mutable on-chain LP balance, not the tracked stake ledger.
The safety invariant is straightforward: a staker’s reward share should remain proportional to the staking system’s tracked total LP stake, and unrelated token withdrawals must not let any user amplify reward-per-token. The code-level breakpoint occurs when the attacker calls withdrawTeam(LP) after staking LP. That call drains the LP held by sDAO, but userLPStakeAmount stays high. When the attacker later calls getReward(), pendingToken() multiplies the large recorded stake by a reward-per-token value derived from an attacker-controlled dust denominator, producing an outsized payout from sDAO’s token inventory.
4. Detailed Root Cause Analysis
The verified victim code shows the two vulnerable behaviors directly:
function withdrawTeam(address _token) external {
IERC20(_token).transfer(TEAM, IERC20(_token).balanceOf(address(this)));
payable(TEAM).transfer(address(this).balance);
}
function getPerTokenReward() public view returns(uint) {
if ( LPInstance.balanceOf(address(this)) == 0) {
return 0;
}
uint newPerTokenReward =
(totalStakeReward - lastTotalStakeReward) * 1e18 / LPInstance.balanceOf(address(this));
return PerTokenRewardLast + newPerTokenReward;
}
Observed exploit flow from the seed trace:
flashLoan(0, 500000000000000000000, attacker, ...)
swapExactTokensForTokensSupportingFeeOnTransferTokens(250000000000000000000, ...)
addLiquidity(sDAO, USDT, ...)
stakeLP(241980243966583129219)
withdrawTeam(PancakePair)
PancakePair::transfer(TEAM, 1893980243966583129219)
PancakePair::transfer(sDAO, 13000000000000000)
getReward()
swapExactTokensForTokensSupportingFeeOnTransferTokens(3698480387757676166673476, ...)
USDT::transfer(flashLoanPool, 500000000000000000000)
The transaction evidence and balance diff make the accounting failure concrete:
- Before the exploit, sDAO held
1652000000000000000000LP. - The attacker staked
241980243966583129219LP throughstakeLP. withdrawTeam(LP)transferred1893980243966583129219LP from sDAO toTEAM.- The attacker restored only
13000000000000000LP to sDAO before callinggetReward(). - The trace then shows a reward-driven exit swap of
3698480387757676166673476sDAO into USDT.
The balance diff confirms the end state:
{
"token": "0x333896437125ff680f146f18c8a164be831c4c71",
"holder": "0x6666625ab26131b490e7015333f97306f05bf816",
"before": "1652000000000000000000",
"after": "13000000000000000",
"delta": "-1651987000000000000000"
}
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x2b9eff2f254662e0f16b9adc249aaa509b1c58d4",
"before": "0",
"after": "13661918634705551920897",
"delta": "13661918634705551920897"
}
This is sufficient to validate the reported mechanism: the exploit does not depend on privileged access or hidden state, only on public contracts and the faulty interaction between unrestricted LP withdrawal and live-balance reward accounting.
5. Adversary Flow Analysis
The adversary cluster consists of EOA 0xa1b6d1f23931911ecd1920df49ee7a79cf7b8983 and helper contract 0x2b9eff2f254662e0f16b9adc249aaa509b1c58d4. The EOA submitted the exploit transaction and paid gas. The helper contract executed the flash-loan callback, interacted with PancakeRouter and sDAO, and retained the profit.
The execution stages were:
- Flash-loan funding and sDAO acquisition. The helper borrowed
500USDT-equivalent units (500000000000000000000) from0x26d0c625e5f5d6de034495fbde1f6e9377185618, then swapped250USDT for sDAO through PancakeRouter. - LP creation and staking. The helper added liquidity to the sDAO/USDT pair, received LP, and staked
241980243966583129219LP into sDAO. - Denominator collapse. The helper called
withdrawTeam(LP), which moved sDAO’s LP inventory toTEAM, then transferred back only13000000000000000LP to leave a dust denominator. - Reward realization and exit. The helper called
getReward(), received the inflated sDAO payout, swapped the reward back to USDT, repaid the flash loan principal, and kept the residual USDT balance as profit.
Every stage is visible in the trace and uses publicly callable code paths, which is why the incident is properly classified as ACT.
6. Impact & Losses
The economically realized loss captured by the attacker helper contract was 13661918634705551920897 USDT units (18 decimals) within the single exploit transaction. The immediate source of value was the inflated transfer of sDAO reward tokens out of the protocol’s token inventory, followed by conversion into USDT through the sDAO/USDT market.
The exploit also moved nearly all LP tokens held by sDAO to the TEAM address during the attack path, demonstrating that the staking contract’s LP custody and reward accounting assumptions were both broken at the same time.
7. References
- Seed transaction:
0xb3ac111d294ea9dedfd99349304a9606df0b572d05da8cedf47ba169d10791ed - Verified sDAO contract:
0x6666625ab26131b490e7015333f97306f05bf816 - Pancake LP pair:
0x333896437125ff680f146f18c8a164be831c4c71 - Flash-loan pool:
0x26d0c625e5f5d6de034495fbde1f6e9377185618 - TEAM recipient:
0xd9f90567162bcc6999265b1f1d5f77490c2dfeaa - Evidence used:
- Collector metadata for the seed transaction
- Collector opcode-level trace for the seed transaction
- Collector balance diff for the seed transaction
- Verified sDAO source code