All incidents

Stax Migration Drain

Share
Oct 11, 2022 13:08 UTCAttackLoss: 321,154.87 xFraxTempleLPPending manual check1 exploit txWindow: Atomic
Estimated Impact
321,154.87 xFraxTempleLP
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Oct 11, 2022 13:08 UTC → Oct 11, 2022 13:08 UTC

Exploit Transactions

TX 1Ethereum
0x8c3f442fc6d640a6ff3ea0b12be64f1d4609ea94edd2966f42c01cd9bdcf04b5
Oct 11, 2022 13:08 UTCExplorer

Victim Addresses

0xd2869042e12a3506100af1d192b5b04d65137941Ethereum

Loss Breakdown

321,154.87xFraxTempleLP

Similar Incidents

Root Cause Analysis

Stax Migration Drain

1. Incident Overview TL;DR

Stax Finance lost the full xFraxTempleLP balance held by its staking contract in transaction 0x8c3f442fc6d640a6ff3ea0b12be64f1d4609ea94edd2966f42c01cd9bdcf04b5 on Ethereum block 15725067. The attacker used a permissionless migration path in StaxLPStaking at 0xd2869042e12a3506100af1d192b5b04d65137941 to mint an unbacked internal stake balance and then immediately redeem it for real pool assets.

The root cause is that migrateStake(address oldStaking, uint256 amount) trusts an arbitrary caller-supplied oldStaking contract. It treats a successful external call to migrateWithdraw as proof that tokens were migrated, then credits stake via _applyStake without verifying that stakingToken was actually received.

2. Key Background

StaxLPStaking tracks staking positions with internal accounting variables while separately custodying the real xFraxTempleLP ERC20 token at 0xbcb8b7fc9197feda75c101fa69d3211b5a30dcd9. Users can redeem stake through withdraw and withdrawAll, and those functions transfer real tokens based on the internal _balances ledger.

The contract also exposes a migration path intended to move stake from an old staking contract into the new one. In the verified source, that flow is:

function migrateStake(address oldStaking, uint256 amount) external {
    StaxLPStaking(oldStaking).migrateWithdraw(msg.sender, amount);
    _applyStake(msg.sender, amount);
}

The security requirement for this design is straightforward: each credited stake unit must be backed by a matching increase in the contract's real token holdings before it becomes withdrawable.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an accounting mismatch in an externally driven migration flow. migrateStake allows any caller to choose oldStaking, makes an unverified external call, and then increases _totalSupply and _balances[msg.sender] through _applyStake. No code checks that the victim contract received amount xFraxTempleLP from the supplied source contract. Because withdrawAll(false) later relies on the credited internal balance instead of re-checking asset backing, the credited balance becomes immediately redeemable for real tokens already held by the pool. The broken invariant is that each withdrawable stake unit should remain backed 1:1 by xFraxTempleLP held by the contract. The code-level breakpoint is the unconditional _applyStake(msg.sender, amount) immediately after migrateWithdraw. This is a deterministic ACT bug because any unprivileged actor can deploy a helper contract that returns successfully from migrateWithdraw without transferring tokens.

4. Detailed Root Cause Analysis

Verified source from Etherscan confirms that the victim contract exposes migrateStake, _applyStake, withdrawAll, and migrateWithdraw. The critical sequence is:

function _applyStake(address _for, uint256 _amount) internal updateReward(_for) {
    _totalSupply += _amount;
    _balances[_for] += _amount;
    emit Staked(_for, _amount);
}

function withdrawAll(bool claim) external {
    _withdrawFor(msg.sender, msg.sender, _balances[msg.sender], claim, msg.sender);
}

The seed trace shows the attacker contract first queried the victim's token balance, then called:

StaxLP::balanceOf(StaxLPStaking)
StaxLPStaking::migrateStake(0x9bdb04493aF17eB318A23BfeFe43f07b3E58EcFb, 321154865567124596801893)
0x9bdb04493aF17eB318A23BfeFe43f07b3E58EcFb::migrateWithdraw(...)
StaxLPStaking::withdrawAll(false)

Etherscan contract-creation data shows 0x9bdb04493af17eb318a23bfefe43f07b3e58ecfb was attacker-deployed by 0xd3127a793daf3aa9424525e893b494d17e798d39. Its creation bytecode exposes the 0x3c24436c selector, which corresponds to migrateWithdraw(address,uint256), and the helper does not transfer staking tokens into the victim. The victim therefore credited 321154865567124596801893 stake units without receiving new xFraxTempleLP.

Once credited, the attacker could immediately invoke withdrawAll(false). The collector trace records the real asset transfer:

emit Transfer(
  from: StaxLPStaking [0xd2869042e12a3506100af1d192b5b04d65137941],
  to:   0x2Df9c154fe24D081cfE568645Fb4075d725431e0,
  value: 321154865567124596801893
)
emit Withdrawn(... amount: 321154865567124596801893)

That transfer drained the victim's full pre-state token balance. The exploit succeeds because internal stake issuance was decoupled from verifiable token receipt.

5. Adversary Flow Analysis

The adversary flow contains three stages. First, the attacker cluster deployed two helper contracts: 0x9bdb04493af17eb318a23bfefe43f07b3e58ecfb as the fake oldStaking target and 0x2df9c154fe24d081cfe568645fb4075d725431e0 as the exploit coordinator. Both were created by 0xd3127a793daf3aa9424525e893b494d17e798d39, and the final exploit transaction was sent by EOA 0x9c9fb3100a2a521985f0c47de3b4598dafd25b01.

Second, in the seed transaction, the exploit contract measured the victim's current xFraxTempleLP balance and used that exact value as the migration amount. It then called migrateStake(fakeOldStaking, fullPoolBalance). Because the fake helper returned successfully, the victim increased the attacker's internal stake balance even though no migration tokens were delivered.

Third, the exploit contract called withdrawAll(false) against the same victim contract. That function redeemed the fabricated internal balance for the real xFraxTempleLP already held by the staking contract, transferring the entire pool to the attacker-controlled contract in a single transaction.

6. Impact & Losses

The immediate loss was the full xFraxTempleLP balance held by the staking contract:

xFraxTempleLP: 321154865567124596801893

This broke the 1:1 backing between user stake balances and actual staking tokens held by the contract. The balance-diff artifact also shows the gas payer EOA spent 13260171153748008 wei in transaction fees, but the economically relevant exploit result is the transfer of the full xFraxTempleLP pool out of the victim contract.

7. References

  • Seed exploit transaction: 0x8c3f442fc6d640a6ff3ea0b12be64f1d4609ea94edd2966f42c01cd9bdcf04b5
  • Related helper deployment transactions: 0xa28444f3bcb81947889e34b1e14c07c0c273d28a90aabc37da51636c490de19f, 0x34d996b92754dc3895a52bca07f6785d62d161f95b364f81d2c5d87e40cd44c8
  • Victim contract: 0xd2869042e12a3506100af1d192b5b04d65137941
  • Staking token: 0xbcb8b7fc9197feda75c101fa69d3211b5a30dcd9
  • Collector trace and metadata for the seed transaction
  • Etherscan verified source for StaxLPStaking
  • Etherscan contract-creation records for 0x2df9c154fe24d081cfe568645fb4075d725431e0 and 0x9bdb04493af17eb318a23bfefe43f07b3e58ecfb