All incidents

StakingDYNA Reward Backdating Drain

Share
Feb 15, 2023 10:57 UTCAttackLoss: 225,350,547.3 DYNAPending manual check8 exploit txWindow: 6d 18h
Estimated Impact
225,350,547.3 DYNA
Label
Attack
Exploit Tx
8
Addresses
2
Attack Window
6d 18h
Feb 15, 2023 10:57 UTC → Feb 22, 2023 05:36 UTC

Exploit Transactions

TX 1BSC
0x06bbe093d9b84783b8ca92abab5eb8590cb2321285660f9b2a529d665d3f18e4
Feb 15, 2023 10:57 UTCExplorer
TX 2BSC
0x402ae286da42830b95c2371fdcf115b361d166db3c3721e2d43cfcecd12b3675
Feb 15, 2023 11:11 UTCExplorer
TX 3BSC
0xd1e8818b1c56b3bb82398d2a49d1abe2c4f1f366463bfd44165a4e216439128b
Feb 15, 2023 11:12 UTCExplorer
TX 4BSC
0x60d665890c1ec6c698eca24174465179e04258cdf34d3d4e585e85a8b237924f
Feb 15, 2023 11:13 UTCExplorer
TX 5BSC
0x64d45176c79d9616c9159f67507873b3a2119e50387edccab7b98afdfc45265c
Feb 15, 2023 11:13 UTCExplorer
TX 6BSC
0x7cda1803f582481306201820757617da0472ad33164d784d4d397b9c05ef99fb
Feb 15, 2023 11:13 UTCExplorer
TX 7BSC
0x7fa89d869fd1b89ee07c206c3c89d6169317b7de8b020edd42402d9895f0819e
Feb 15, 2023 11:14 UTCExplorer
TX 8BSC
0xc09678fec49c643a30fc8e4dec36d0507dae7e9123c270e1f073d335deab6cf0
Feb 22, 2023 05:36 UTCExplorer

Victim Addresses

0xa7b5eabc3ee82c585f5f4ccc26b81c3bd62ff3a9BSC
0x5c0d0111ffc638802c9efccf55934d5c63ab3f79BSC

Loss Breakdown

225,350,547.3DYNA

Similar Incidents

Root Cause Analysis

StakingDYNA Reward Backdating Drain

1. Incident Overview TL;DR

On BNB Smart Chain, an unprivileged attacker drained 225350547299933433473959064 DYNA from StakingDYNA at 0xa7b5eabc3ee82c585f5f4ccc26b81c3bd62ff3a9. The attacker first deployed helper contract 0xd360b416ce273ab2358419b1015acf476a3b30d9, then used seven priming transactions to create 700 aged worker-specific stakes, and finally executed tx 0xc09678fec49c643a30fc8e4dec36d0507dae7e9123c270e1f073d335deab6cf0 to recycle one large DYNA balance across 258 workers. Each worker deposited a large amount and immediately redeemed it, extracting fabricated rewards that were then sold through the public DYNA/WBNB Pancake pair 0xb6148c6fa6ebdd6e22ef5150c5c3cee78b24a3a0.

The root cause is a reward-accounting bug in StakingDYNA.deposit(uint256). On a repeat deposit, the contract increases principal but does not crystallize accrued rewards or refresh lastProcessAt. The next getInterest() call therefore applies the full elapsed time to newly added principal, which lets any actor with an aged stake backdate a fresh deposit and redeem an immediate profit.

2. Key Background

StakingDYNA is a DYNA staking contract with a fixed apr = 3200, meaning 32% annualized rewards with RATE_PRECISION = 10000. Reward state is tracked per msg.sender in a StakeDetail struct containing principal, pendingReward, lastProcessAt, and firstStakeAt. Rewards are not minted; they are paid directly from the staking contract's existing DYNA balance, so any accounting error transfers real value out of the pool.

This design makes address separation important. Because each worker contract has its own StakeDetail, an attacker can manufacture many independent aged positions by deploying many simple contracts, funding each with a small DYNA amount, and calling deposit() once per worker. The collected attacker history shows exactly that pattern: helper deployment in tx 0x0cb3f64df060d97192002ae4500c83c2334bb95bb273c5ae8aa1c6446936ca1f, funding and setup, then seven helper calls that each prime 100 workers.

The monetization path was public and permissionless. DYNA had live liquidity against WBNB on Pancake, and the exploit transaction ended by swapping extracted DYNA through router 0x10ED43C718714eb63d5aA57B78B54704E256024E, sending 73.846655167007440408 WBNB to attacker profit EOA 0x35596bc57c0cab856b87854ecc142020a47f6fdf.

3. Vulnerability Analysis & Root Cause Summary

The vulnerable branch is the repeated-deposit path in StakingDYNA.deposit(uint256). On the first stake, the contract sets both firstStakeAt and lastProcessAt to the current block timestamp. On later deposits, it only adds _stakeAmount into principal. It does not first move accrued rewards into pendingReward, and it does not reset lastProcessAt.

The verified source snapshot shows the bug directly:

function getInterest(address _staker) public view returns (uint256) {
    StakeDetail memory stakeDetail = stakers[_staker];
    uint256 duration = block.timestamp.sub(stakeDetail.lastProcessAt);
    uint256 interest = stakeDetail
        .principal
        .mul(apr)
        .mul(duration)
        .div(ONE_YEAR_IN_SECONDS)
        .div(RATE_PRECISION);
    return interest.add(stakeDetail.pendingReward);
}

function deposit(uint256 _stakeAmount) external {
    token.transferFrom(msg.sender, address(this), _stakeAmount);
    StakeDetail storage stakeDetail = stakers[msg.sender];
    if (stakeDetail.firstStakeAt == 0) {
        stakeDetail.principal = stakeDetail.principal.add(_stakeAmount);
        stakeDetail.firstStakeAt = block.timestamp;
        stakeDetail.lastProcessAt = block.timestamp;
    } else {
        stakeDetail.principal = stakeDetail.principal.add(_stakeAmount);
    }
}

The broken invariant is straightforward: rewards over interval [lastProcessAt, t] must only apply to principal that was staked during that interval. StakingDYNA violates that invariant by letting fresh principal inherit an old lastProcessAt. The code-level breakpoint is the else branch in deposit() where principal changes but reward checkpoint state does not.

4. Detailed Root Cause Analysis

The exploit relies on two phases: first create aged stakes, then backdate large deposits against those old timestamps.

In the priming phase, tx 0x06bbe093d9b84783b8ca92abab5eb8590cb2321285660f9b2a529d665d3f18e4 shows the helper distributing small DYNA amounts into worker contracts and each worker calling deposit(). The receipt contains 100 Deposit events on StakingDYNA, and six later helper calls repeat the same pattern, producing 700 primed workers in total. A representative worker is 0x0196395b9f72c210bd772119d249276c987316f1, which was primed in that first batch.

In the exploit phase, the attacker waited about 585528 seconds, then ran tx 0xc09678fec49c643a30fc8e4dec36d0507dae7e9123c270e1f073d335deab6cf0. For worker 0x0196395b9f72c210bd772119d249276c987316f1, the aged pre-state was principal = 9.509900499 DYNA and lastProcessAt = 1676458664. Inside the exploit trace, that worker immediately deposited 64518684.750699557630064907 DYNA and then redeemed 64902018.047956864688208150 DYNA.

That overpayment matches the buggy formula exactly. With total principal after the new deposit equal to 64518694.260600056630064907 DYNA, APR 32%, and dt = 585528, the contract computes total accrued interest of 383333.353759722272536668 DYNA. Redeeming only the freshly added amount yields a claim of 383333.297257307058143243 DYNA, so the returned amount is:

fresh deposit = 64518684.750699557630064907 DYNA
claim amount  =   383333.297257307058143243 DYNA
redeem total  = 64902018.047956864688208150 DYNA

That is the exact Redeem amount emitted on-chain. The exploit therefore is not an approximation or market effect; it is the deterministic output of the buggy accounting path.

The exploit trace also shows that the helper contract immediately forwards the returned DYNA to the next worker and repeats the sequence. The same principal is recycled across 258 pre-aged workers, so the attacker compounds fabricated rewards without needing 258 independent large bankrolls. Because redeem() pays from the staking contract's existing DYNA balance, each iteration directly depletes StakingDYNA.

5. Adversary Flow Analysis

The attacker-controlled cluster is:

  • EOA 0x0c925a25fdaac4460cab0cc7abc90ff71f410094: deployed the helper and submitted every priming and exploit transaction.
  • Helper 0xd360b416ce273ab2358419b1015acf476a3b30d9: orchestrated worker deployment, staking, recycling, and final swap.
  • Many worker contracts such as 0x0196395b9f72c210bd772119d249276c987316f1: each held its own aged staking record.
  • Profit EOA 0x35596bc57c0cab856b87854ecc142020a47f6fdf: received WBNB proceeds.

The end-to-end sequence is:

  1. 0x0cb3f64df060d97192002ae4500c83c2334bb95bb273c5ae8aa1c6446936ca1f The attacker deploys helper contract 0xd360....
  2. 0x06bbe093d9b84783b8ca92abab5eb8590cb2321285660f9b2a529d665d3f18e4 First priming batch. The receipt shows 100 worker Deposit events into StakingDYNA.
  3. 0x402ae286da42830b95c2371fdcf115b361d166db3c3721e2d43cfcecd12b3675
  4. 0xd1e8818b1c56b3bb82398d2a49d1abe2c4f1f366463bfd44165a4e216439128b
  5. 0x60d665890c1ec6c698eca24174465179e04258cdf34d3d4e585e85a8b237924f
  6. 0x64d45176c79d9616c9159f67507873b3a2119e50387edccab7b98afdfc45265c
  7. 0x7cda1803f582481306201820757617da0472ad33164d784d4d397b9c05ef99fb
  8. 0x7fa89d869fd1b89ee07c206c3c89d6169317b7de8b020edd42402d9895f0819e Six more priming batches, taking the worker set to 700 aged stakes.
  9. 0xc09678fec49c643a30fc8e4dec36d0507dae7e9123c270e1f073d335deab6cf0 The helper cycles DYNA through 258 aged workers, collects the fabricated rewards, and swaps DYNA into WBNB.

The exploit trace excerpt below captures the decisive flow inside the final transaction:

emit Deposit(user: 0x0196395b9f72c210bd772119d249276c987316f1, amount: 64518684750699557630064907)
emit Redeem(user: 0x0196395b9f72c210bd772119d249276c987316f1, amount: 64902018047956864688208150)
...
0x10ED43C718714eb63d5aA57B78B54704E256024E::swapExactTokensForTokensSupportingFeeOnTransferTokens(
  216581548421891486166916268,
  0,
  [DYNA, WBNB],
  0x35596bc57c0Cab856b87854EcC142020A47f6fdF,
  1677044192
)
...
emit Transfer(from: PancakePair: [0xb6148c6fA6Ebdd6e22eF5150c5C3ceE78b24a3a0], to: 0x35596bc57c0Cab856b87854EcC142020A47f6fdF, value: 73846655167007440408)

The attacker did not need any privileged access. Every step used public contracts, attacker-deployed helper code, and standard token transfers and swaps.

6. Impact & Losses

The measured protocol loss is 225350547299933433473959064 DYNA, taken directly from the collector balance diff for exploit tx 0xc09678fec49c643a30fc8e4dec36d0507dae7e9123c270e1f073d335deab6cf0. The staking pool balance changed from 225352968127419151994856842 DYNA before the exploit to 2420827485718520897778 DYNA after it. This is effectively a full drain of the reward inventory held by StakingDYNA.

The attacker monetized the stolen DYNA on the live DYNA/WBNB market. The exploit receipt and trace show a direct transfer of 73846655167007440408 wei of WBNB, equal to 73.846655167007440408 WBNB, to profit EOA 0x35596bc57c0cab856b87854ecc142020a47f6fdf. The sender EOA separately paid 0.285331529 BNB in gas and the attacker cluster still remained strongly net positive.

The incident also caused secondary market harm. Because the attacker dumped extracted DYNA into public Pancake liquidity, the exploit converted the accounting failure into immediate market impact for DYNA holders in addition to draining the staking pool.

7. References

  • Victim staking contract: StakingDYNA at 0xa7b5eabc3ee82c585f5f4ccc26b81c3bd62ff3a9
  • DYNA token: 0x5c0d0111ffc638802c9efccf55934d5c63ab3f79
  • Public monetization venue: Pancake DYNA/WBNB pair 0xb6148c6fa6ebdd6e22ef5150c5c3cee78b24a3a0
  • Verified victim source used for validation: StakingDYNA source snapshot collected from the explorer
  • Attacker history used for validation: EOA tx list for 0x0c925a25fdaac4460cab0cc7abc90ff71f410094
  • Priming transaction sample: 0x06bbe093d9b84783b8ca92abab5eb8590cb2321285660f9b2a529d665d3f18e4
  • Exploit transaction: 0xc09678fec49c643a30fc8e4dec36d0507dae7e9123c270e1f073d335deab6cf0
  • Supporting evidence types reviewed: collected traces, transaction metadata, balance diffs, exploit receipt logs, and verified source code