All incidents

USDTStaking Approval Drain

Share
Jul 15, 2023 04:54 UTCAttackLoss: 20,999.92 USDTPending manual check1 exploit txWindow: Atomic
Estimated Impact
20,999.92 USDT
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Jul 15, 2023 04:54 UTC → Jul 15, 2023 04:54 UTC

Exploit Transactions

TX 1Ethereum
0xfc872bf5ca8f04b18b82041ec563e4abf2e31e1fc27d1ea5dee39bc8a79d2d06
Jul 15, 2023 04:54 UTCExplorer

Victim Addresses

0x800cfd4a2ba8ce93ea2cc814fce26c3635169017Ethereum

Loss Breakdown

20,999.92USDT

Similar Incidents

Root Cause Analysis

USDTStaking Approval Drain

1. Incident Overview TL;DR

Transaction 0xfc872bf5ca8f04b18b82041ec563e4abf2e31e1fc27d1ea5dee39bc8a79d2d06 drained the full live USDT balance from USDTStakingContract28 at 0x800cfd4a2ba8ce93ea2cc814fce26c3635169017 on Ethereum mainnet block 17696563. The attacker used a permissionless call path to make the victim contract approve an attacker-controlled spender and then immediately pulled the victim's entire USDT balance with transferFrom.

The root cause is a direct access-control failure. tokenAllowAll(address asset, address allowee) is public and unguarded, even though the same contract protects transferAllFunds() with onlyOwner. That mistake lets any unprivileged caller mint unlimited ERC20 allowance from the victim contract to an arbitrary spender for any ERC20 the contract holds.

2. Key Background

USDTStakingContract28 is a staking-style contract that keeps depositors' USDT at the contract address itself and pays withdrawals with _token.safeTransfer(...). The verified source also contains an explicit owner-only administrative withdrawal path, which establishes the intended authorization boundary for token outflows.

USDT supports the standard allowance model. Once the victim contract approves a spender, that spender can move tokens out of the victim address with transferFrom. That is why exposing an approval primitive is security-sensitive even if the function does not transfer tokens directly.

The ACT framing is straightforward here: the victim address, the token address, the vulnerable function, and the victim's live USDT balance were all publicly observable on-chain before the exploit transaction. No attacker private key, privileged role, or proprietary artifact was required.

3. Vulnerability Analysis & Root Cause Summary

This is an ATTACK-class incident caused by missing access control on a sensitive approval helper. The verified victim source shows that transferAllFunds() is protected by onlyOwner, but tokenAllowAll(address asset, address allowee) is public and accepts attacker-chosen parameters. Inside that function, the contract checks the current allowance and then executes token.safeApprove(allowee, uint256(-1)) from the victim contract's own balance context.

That behavior violates the core invariant for ERC20 custody contracts: only authorized protocol logic should be able to create or extend token allowances from the contract's balance. Here, any caller can select asset = USDT and allowee = attacker-controlled address, converting the victim contract into the signer of an unlimited approval. Once that approval exists, draining the victim is trivial because USDT honors transferFrom. The exploit therefore does not depend on a hidden pricing bug, race condition, or privileged helper; it is a direct consequence of exposing an internal approval primitive as a public interface.

4. Detailed Root Cause Analysis

The verified victim source shows the intended authorization boundary and the exact breakpoint:

modifier onlyOwner {
    require(msg.sender == _owner, "Not the contract owner.");
    _;
}

function transferAllFunds() external onlyOwner {
    uint256 contractBalance = _token.balanceOf(address(this));
    require(contractBalance > 0, "No funds to transfer.");
    _token.safeTransfer(_owner, contractBalance);
}

function tokenAllowAll(address asset, address allowee) public {
    IERC20 token = IERC20(asset);

    if (token.allowance(address(this), allowee) != uint256(-1)) {
        token.safeApprove(allowee, uint256(-1));
    }
}

Source: verified USDTStakingContract28 code collected from Etherscan.

The relevant pre-state was also deterministic. Immediately before block 17696563, the victim held 20999916289 raw USDT units, and neither the attacker sender nor the helper already had allowance:

block_pre=17696562
victim_usdt_balance=20999916289
allowance_victim_to_helper=0
allowance_victim_to_sender=0

Source: pre-state checks on the forked mainnet state.

The call trace then shows the exploit unfolding in one transaction:

0x800cfD4A2ba8CE93eA2cc814Fce26c3635169017::tokenAllowAll(
  TetherToken: [0xdAC17F958D2ee523a2206206994597C13D831ec7],
  0xb754EBdBa9B009113b4cF445a7cb0FC9227648aD
)
TetherToken::approve(
  0xb754EBdBa9B009113b4cF445a7cb0FC9227648aD,
  115792089237316195423570985008687907853269984665640564039457584007913129639935
)
TetherToken::balanceOf(0x800cfD4A2ba8CE93eA2cc814Fce26c3635169017) => 20999916289
TetherToken::transferFrom(
  0x800cfD4A2ba8CE93eA2cc814Fce26c3635169017,
  0x000000915F1B10B0EF5c4EFE696Ab65f13F36E74,
  20999916289
)

Source: collected execution trace for the exploit transaction.

The transaction receipt confirms the same state transition at the event layer: USDT emitted an Approval from the victim to the helper at uint256.max, followed by a Transfer of 20999916289 raw units from the victim to attacker EOA 0x000000915f1b10b0ef5c4efe696ab65f13f36e74.

Profit realization was also fully determined. The attacker EOA held 0 raw USDT units before block 17696563 and 20999916289 raw units after the exploit block. Gas paid by the sender was 98786 * 26950314220 = 2662313740536920 wei, which was paid separately in ETH rather than USDT. The exploit therefore realized a direct USDT gain of 20999.916289 USDT on the recipient EOA.

5. Adversary Flow Analysis

The attacker strategy was a single-transaction approval-then-pull drain:

  1. EOA 0x000000915f1b10b0ef5c4efe696ab65f13f36e74 sent transaction 0xfc872bf5ca8f04b18b82041ec563e4abf2e31e1fc27d1ea5dee39bc8a79d2d06 to helper contract 0xb754ebdba9b009113b4cf445a7cb0fc9227648ad.
  2. The helper invoked USDTStakingContract28.tokenAllowAll(USDT, helper).
  3. Because tokenAllowAll is public and unprotected, the victim contract approved the helper for unlimited USDT spending.
  4. The helper queried USDT.balanceOf(victim) to compute the runtime drain amount instead of relying on a hardcoded historical balance.
  5. The helper called USDT.transferFrom(victim, attacker_eoa, victimBalance) and moved the full 20999916289 raw USDT units to the attacker EOA.

The attacker-related accounts identified in the evidence are:

  • 0x000000915f1b10b0ef5c4efe696ab65f13f36e74: sender EOA and direct profit recipient.
  • 0xb754ebdba9b009113b4cf445a7cb0fc9227648ad: helper contract that obtained the allowance and executed the transfer path.

The victim contract candidate is:

  • 0x800cfd4a2ba8ce93ea2cc814fce26c3635169017 (USDTStakingContract28), verified on Etherscan.

6. Impact & Losses

The exploit drained the victim contract's entire observed USDT balance in the incident transaction:

  • Token: USDT
  • Raw amount: 20999916289
  • Decimal precision: 6
  • Human-readable amount: 20999.916289 USDT

The immediate impact was the loss of all USDT held by the staking contract at the time of exploitation. The broader impact is worse than a one-off drain: because tokenAllowAll is public and asset-agnostic, any ERC20 balance later held by the same contract remains permissionlessly drainable until the function is removed or access-controlled.

7. References

  1. Exploit transaction: 0xfc872bf5ca8f04b18b82041ec563e4abf2e31e1fc27d1ea5dee39bc8a79d2d06
  2. Victim contract: 0x800cfd4a2ba8ce93ea2cc814fce26c3635169017
  3. USDT token: 0xdAC17F958D2ee523a2206206994597C13D831ec7
  4. Verified victim source collected from Etherscan, including onlyOwner, transferAllFunds(), and tokenAllowAll(...)
  5. Pre-state checks showing victim USDT balance and zero pre-approval before block 17696563
  6. Execution trace showing tokenAllowAll, approve, balanceOf, and transferFrom
  7. Transaction receipt showing the Approval and Transfer events
  8. Profit checks showing the attacker EOA's pre/post USDT balances and exact ETH gas fee