All incidents

STRAC Callback Drain

Share
Jun 27, 2023 18:22 UTCAttackLoss: 130.97 STRACPending manual check2 exploit txWindow: 12s
Estimated Impact
130.97 STRAC
Label
Attack
Exploit Tx
2
Addresses
1
Attack Window
12s
Jun 27, 2023 18:22 UTC → Jun 27, 2023 18:22 UTC

Exploit Transactions

TX 1BSC
0x4aa055ed8d1e6905f3dc4ee4bc75809d61a592e61e24da3460fdc9e836459674
Jun 27, 2023 18:22 UTCExplorer
TX 2BSC
0x1147b3c0f3ebdd524c4e58430bb736eba9f7fa522158f5ad81eb3e2394b466d0
Jun 27, 2023 18:22 UTCExplorer

Victim Addresses

0x1f90bdeb5674833868ee9b36707b929024e7a513BSC

Loss Breakdown

130.97STRAC

Similar Incidents

Root Cause Analysis

STRAC Callback Drain

1. Incident Overview TL;DR

On BNB Smart Chain block 29474566, attacker EOA 0xab90a897cf6c56c69a4579ead3c900260dfba02d used helper contract 0xc7823188d459e1744c0e5fd58a0e074e92982ea3 to call selector 0x4a75084c on STRAC custody contract 0x1f90bdeb5674833868ee9b36707b929024e7a513. That call drained the victim's entire STRAC balance, 130968947172476368780, and the attacker immediately sold the drained tokens through public pair 0x2976bd3774622367ce7a575d28201480e640966f for 12162940372138517923 BEP20Ethereum.

The root cause is an arbitrary-callback withdrawal path inside 0x1f90...a513. The function accepts an attacker-chosen contract, invokes selector 0x23b872dd on it, and then transfers STRAC to msg.sender. Because the callback target is not pinned to trusted storage and the return value is not used as a meaningful authorization signal, any unprivileged actor can deploy a permissive helper and drain the victim's STRAC holdings.

2. Key Background

The victim contract is unverified, so the analysis relies on creation data, runtime behavior, and storage. The creation record ties 0x1f90bdeb5674833868ee9b36707b929024e7a513 to deployment tx 0xcc7f1e698752be78fa081156cf1ca1abd3756b7deff44d8449671b3e9e28be2c, and storage reads at block 29474565 show the critical configuration:

slot0 = 0x000000000000000000000000d1feb6b0c23f36798d9c6c6a88ec7a7a309d59ef
slot3 = 0x0000000000000000000000009801da0aa142749295692c7cb3241e4ee2b80bda
slot4 = 0x0000000000000000000000009801da0aa142749295692c7cb3241e4ee2b80bda

Those slots establish that the contract is owner-configured but hardwired to STRAC token 0x9801da0aa142749295692c7cb3241e4ee2b80bda. The public STRAC/BEP20Ethereum pair 0x2976bd3774622367ce7a575d28201480e640966f already had live liquidity at the same pre-state block, so drained STRAC was immediately monetizable by any caller.

STRAC itself charges a fee on some transfers. The verified token source shows why the pair received slightly less than the full drained amount:

if(_requiredFee[_msgSender()]){
    fee += (amount * _fromFeeRate / 1000);
}
if(_requiredFee[recipient]){
    fee += (amount * _toFeeRate / 1000);
}

unchecked {
    _transfer(_msgSender(), recipient, amount - fee);
    if(fee>0){
        _transfer(_msgSender(), address(this), fee);
    }
}

That fee logic matches the observed post-drain swap leg: 130576040330958939674 STRAC reached the pair, while 392906841517429106 STRAC accrued to the STRAC token contract.

3. Vulnerability Analysis & Root Cause Summary

This is an ATTACK-category custody failure, not a pricing anomaly or privileged-only issue. The vulnerable function is selector 0x4a75084c on 0x1f90bdeb5674833868ee9b36707b929024e7a513, which implements a STRAC-specific withdrawal path. The function checks that its second argument matches the STRAC address stored in slot 3, then performs a low-level call to an attacker-supplied first argument using selector 0x23b872dd, and finally performs a low-level STRAC transfer(address,uint256) to msg.sender.

The broken invariant is straightforward: only explicitly authorized logic should be able to move STRAC out of the custody contract, and any external callback in that workflow must be fixed to trusted code. Instead, the victim lets the caller choose the callback target. The meaningful breakpoint is the CALL sequence at runtime PCs 0x0339-0x0422, where the victim first CALLs the user-supplied callback and then CALLs STRAC with selector 0xa9059cbb to transfer the requested amount out.

In practice, the callback need only expose transferFrom(address,address,uint256) and return success. Because the victim does not bind the callback to trusted storage and does not verify that a real token pull occurred, a freshly deployed helper is enough to authorize the victim's outbound STRAC transfer.

4. Detailed Root Cause Analysis

The victim's effective logic, derived from creation bytecode, storage, and the incident trace, is:

function drainLike(address callback, address token, uint256 amount) external {
    require(token == slot3_STRAC);
    callback.call(abi.encodeWithSelector(0x23b872dd, msg.sender, address(this), amount));
    STRAC.call(abi.encodeWithSelector(0xa9059cbb, msg.sender, amount));
}

The trace of the incident transaction shows that exact sequence on-chain:

0xc7823188...::6ec6db55(...)
  0x1F90BDeB...::4a75084c(0xc7823188..., 0x9801da0a..., 130968947172476368780)
    0xc7823188...::transferFrom(0xc7823188..., 0x1F90BDeB..., 130968947172476368780) -> true
    Token::transfer(0xc7823188..., 130968947172476368780)
      emit Transfer(from: 0x1F90BDeB..., to: 0xc7823188..., value: 130968947172476368780)

That evidence rules out alternate explanations such as a legitimate allowance-based withdrawal or victim-side authorization. The callback target is the attacker helper itself, and the helper returns true immediately. No trusted contract address is consulted before the victim transfers STRAC away.

The balance diff proves the state transition caused by this flaw:

{
  "victim_strac_before": "130968947172476368780",
  "victim_strac_after": "0",
  "attacker_bep20_eth_before": "0",
  "attacker_bep20_eth_after": "12162940372138517923",
  "attacker_native_gas_paid_wei": "707975000000000"
}

The success predicate is therefore deterministic. In pre-state sigma_B at block 29474565, any unprivileged actor can deploy a helper contract that returns success on transferFrom, call 0x1f90...a513::0x4a75084c(helper, STRAC, victimBalance), receive STRAC, and optionally monetize it through the already-liquid public pair. The governing ACT oracle is unauthorized custody loss: the adversary can make the victim transfer its STRAC balance to an adversary-controlled address.

5. Adversary Flow Analysis

The attack has two on-chain stages.

First, the attacker deployed helper contract 0xc7823188d459e1744c0e5fd58a0e074e92982ea3 in tx 0x4aa055ed8d1e6905f3dc4ee4bc75809d61a592e61e24da3460fdc9e836459674 at block 29474562. The creation record attributes that deployment to EOA 0xab90a897cf6c56c69a4579ead3c900260dfba02d. This stage is fully permissionless: any EOA can deploy an equivalent helper.

Second, in tx 0x1147b3c0f3ebdd524c4e58430bb736eba9f7fa522158f5ad81eb3e2394b466d0, the attacker invoked helper selector 0x6ec6db55, which in turn called the victim and then sold the drained STRAC. The relevant execution fragment is:

victim::4a75084c(helper, STRAC, 130968947172476368780)
  helper::transferFrom(...) -> true
  STRAC::transfer(helper, 130968947172476368780)

STRAC::transfer(pair, 130968947172476368780)
  emit Transfer(helper, pair, 130576040330958939674)
  emit Transfer(helper, STRAC, 392906841517429106)

pair::swap(12162940372138517923, 0, helper, 0x)
  BEP20Ethereum::transfer(helper, 12162940372138517923)

This flow matches the adversary lifecycle described in the audited artifact:

  1. Deploy a permissive helper.
  2. Call the victim's public callback-based drain function with the helper and the hardcoded STRAC token.
  3. Receive the victim's full STRAC balance.
  4. Transfer the drained STRAC into the public pair and swap into BEP20Ethereum.

No attacker-private bytecode, secret permissions, or privileged state transitions are required. The victim's own code exposes the custody loss path, and the liquidity pair supplies the public monetization path.

6. Impact & Losses

The victim address 0x1f90bdeb5674833868ee9b36707b929024e7a513 lost its full STRAC balance in one transaction. The measured token loss is:

[
  {
    "token_symbol": "STRAC",
    "amount": "130968947172476368780",
    "decimal": 18
  }
]

The attacker immediately converted the drained STRAC into 12162940372138517923 BEP20Ethereum. Gas was paid separately in native BNB by the attacker EOA, with a measured native balance delta of 707975000000000 wei. Operationally, the contract ceased to function as safe STRAC custody because any unprivileged caller could repeat the drain whenever the contract held STRAC.

7. References

  1. Seed exploit transaction 0x1147b3c0f3ebdd524c4e58430bb736eba9f7fa522158f5ad81eb3e2394b466d0, including metadata, full trace, and balance diff.
  2. STRAC token source at 0x9801da0aa142749295692c7cb3241e4ee2b80bda, used to confirm fee-on-transfer behavior.
  3. Contract-creation records showing victim contract creation tx 0xcc7f1e698752be78fa081156cf1ca1abd3756b7deff44d8449671b3e9e28be2c and helper deployment tx 0x4aa055ed8d1e6905f3dc4ee4bc75809d61a592e61e24da3460fdc9e836459674.
  4. Storage reads at block 29474565 confirming owner slot 0 and STRAC slots 3 and 4.
  5. Auditor fork test log confirming that a fresh helper contract and fresh attacker address reproduce the same semantic drain on a forked mainnet state.