All incidents

sDAO Reward Inflation Attack

Share
Nov 21, 2022 07:54 UTCAttackLoss: 13,661.92 USDTPending manual check1 exploit txWindow: Atomic
Estimated Impact
13,661.92 USDT
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Nov 21, 2022 07:54 UTC → Nov 21, 2022 07:54 UTC

Exploit Transactions

TX 1BSC
0xb3ac111d294ea9dedfd99349304a9606df0b572d05da8cedf47ba169d10791ed
Nov 21, 2022 07:54 UTCExplorer

Victim Addresses

0x6666625ab26131b490e7015333f97306f05bf816BSC
0x333896437125ff680f146f18c8a164be831c4c71BSC

Loss Breakdown

13,661.92USDT

Similar Incidents

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 through stakeLP.
  • getPerTokenReward() divides newly accrued reward by LPInstance.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 1652000000000000000000 LP.
  • The attacker staked 241980243966583129219 LP through stakeLP.
  • withdrawTeam(LP) transferred 1893980243966583129219 LP from sDAO to TEAM.
  • The attacker restored only 13000000000000000 LP to sDAO before calling getReward().
  • The trace then shows a reward-driven exit swap of 3698480387757676166673476 sDAO 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:

  1. Flash-loan funding and sDAO acquisition. The helper borrowed 500 USDT-equivalent units (500000000000000000000) from 0x26d0c625e5f5d6de034495fbde1f6e9377185618, then swapped 250 USDT for sDAO through PancakeRouter.
  2. LP creation and staking. The helper added liquidity to the sDAO/USDT pair, received LP, and staked 241980243966583129219 LP into sDAO.
  3. Denominator collapse. The helper called withdrawTeam(LP), which moved sDAO’s LP inventory to TEAM, then transferred back only 13000000000000000 LP to leave a dust denominator.
  4. 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