USDTStaking Approval Drain
Exploit Transactions
0xfc872bf5ca8f04b18b82041ec563e4abf2e31e1fc27d1ea5dee39bc8a79d2d06Victim Addresses
0x800cfd4a2ba8ce93ea2cc814fce26c3635169017EthereumLoss Breakdown
Similar Incidents
Vortex approveToken Drain
41%Flooring extMulticall Approval Drain
41%0x7CAE Approved-Spender Drain
36%V3Utils Arbitrary Call Drain
35%Dexible selfSwap allowance drain
35%NOON Pool Drain via Public transfer
34%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:
- EOA
0x000000915f1b10b0ef5c4efe696ab65f13f36e74sent transaction0xfc872bf5ca8f04b18b82041ec563e4abf2e31e1fc27d1ea5dee39bc8a79d2d06to helper contract0xb754ebdba9b009113b4cf445a7cb0fc9227648ad. - The helper invoked
USDTStakingContract28.tokenAllowAll(USDT, helper). - Because
tokenAllowAllis public and unprotected, the victim contract approved the helper for unlimited USDT spending. - The helper queried
USDT.balanceOf(victim)to compute the runtime drain amount instead of relying on a hardcoded historical balance. - The helper called
USDT.transferFrom(victim, attacker_eoa, victimBalance)and moved the full20999916289raw 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
- Exploit transaction:
0xfc872bf5ca8f04b18b82041ec563e4abf2e31e1fc27d1ea5dee39bc8a79d2d06 - Victim contract:
0x800cfd4a2ba8ce93ea2cc814fce26c3635169017 - USDT token:
0xdAC17F958D2ee523a2206206994597C13D831ec7 - Verified victim source collected from Etherscan, including
onlyOwner,transferAllFunds(), andtokenAllowAll(...) - Pre-state checks showing victim USDT balance and zero pre-approval before block
17696563 - Execution trace showing
tokenAllowAll,approve,balanceOf, andtransferFrom - Transaction receipt showing the
ApprovalandTransferevents - Profit checks showing the attacker EOA's pre/post USDT balances and exact ETH gas fee