BCT Referral Treasury Drain
Exploit Transactions
Victim Addresses
0x70ca72bb4a1386439a2a51476f2335a31005ebe8BSC0x8ebeb2bf3c6ca7d5c6b345515e368e94eda0ab26BSC0x5a25b8576b14699bbb15947111f5811e58b39a82BSC0x88b3eb62e363d9f153beab49c5c2ef2e785a375aBSCLoss Breakdown
Similar Incidents
T3913 Pair-Skim Referral Drain
42%SNKMiner Referral Reward Drain
40%GGGTOKEN Treasury Drain via receive()
40%Eterna Buyback Treasury Drain
38%Matmo MAMO Treasury Drain
37%Public Treasury Spend on BRAND Helper
36%Root Cause Analysis
BCT Referral Treasury Drain
1. Incident Overview TL;DR
On BNB Smart Chain, an unprivileged adversary cluster used two public transactions to turn BCT's referral system into a treasury-drain loop. In setup transaction 0xd4c19d575ea5b3a415cc288ce09942299ca3a3b49ef9718cda17e4033dd4c250, the attacker created a five-level inviter chain with dust-sized BCT transfers. In exploit transaction 0xdae0b85e01670e6b6b317657a72fb560fc388664cf8bfdd9e1b0ae88e0679103, the attacker flash-borrowed 20 WBNB, repeatedly sold BCT into BCT's hardcoded BCT/USDT pair, reclaimed the post-fee principal with skim(), harvested treasury-funded referral rewards from payTokenAddress, converted the drained inventory through public pairs, repaid the flash swap, and exited with 10.154043154622408863 WBNB.
The root cause is a protocol bug in BCT. The token recognizes only one AMM pair, the constructor-created BCT/USDT pair, while all other BCT pairs are treated as ordinary transfers. That design sends 50% of non-designated-pair transfers to payTokenAddress, then later pays up to 40% of each sell into the hardcoded pair out of that same treasury via promoteReward, without requiring any irreversible economic loss by the seller. Because direct pair transfers leave excess token balances that can be reclaimed with skim(), the same capital can repeatedly harvest treasury-funded rewards.
2. Key Background
BCT is the contract at 0x70ca72bb4a1386439a2a51476f2335a31005ebe8. Its constructor creates exactly one tracked PancakeSwap pair against USDT, stores that address in uniswapV2Pair, and sets the referral treasury payTokenAddress to 0x8ebeb2bf3c6ca7d5c6b345515e368e94eda0ab26.
The inviter graph is not access-controlled. bindInviter records relationship state using two exact transfer sizes:
1e15BCT marksbeinvited[to] = from5e14BCT finalizesinviter[to] = from
promoteReward then walks up to five inviter levels and transfers treasury BCT from payTokenAddress if each inviter holds at least 30 * 10**18 BCT.
The critical market structure is:
- Hardcoded pair: BCT/USDT at
0x5a25b8576b14699bbb15947111f5811e58b39a82 - External live pair: BCT/WBNB at
0x88b3eb62e363d9f153beab49c5c2ef2e785a375a - Flash-swap pair: WBNB/BUSD at
0x1b96b92314c44b159149f7e0303511fb2fc4774f
Because BCT only recognizes the USDT pair, trades that involve the WBNB pair are processed by the generic transfer branch and continuously refill payTokenAddress.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability class is an application-level economic attack caused by inconsistent pair classification and treasury-funded referral payouts. The constructor binds fee logic to a single USDT pair, but BCT is actively traded in a separate WBNB pair. As a result, BCT/WBNB transfers hit the generic else branch in takeAllFee, which diverts 50% of every such transfer to payTokenAddress. Once that treasury is large, any user can build a five-address inviter chain and qualify each inviter with at least 30 BCT. After qualification, every sell into the hardcoded USDT pair transfers 1% to payTokenAddress, 14% to the token contract, and up to 40% of the nominal sell amount from payTokenAddress to attacker-controlled inviters. The seller does not need to give up the full nominal amount permanently, because direct transfers into the pair leave reclaimable excess balances that can be pulled back with skim(). The exploit therefore converts a reusable trading balance into repeated treasury withdrawals until the attacker can dump the harvested BCT back into public liquidity and exit in WBNB.
The vulnerable components are concentrated in one contract:
- constructor pair selection and treasury initialization in
Contract.sollines 1379-1400 - inviter binding and reward payout in
Contract.sollines 1473-1505 - transfer classification and fee routing in
Contract.sollines 1507-1599
The security principles violated are also concrete:
- referral rewards are not conserved against realized trade value
- pair-specific fee logic is unsafe when only one live AMM pair is recognized
- treasury-funded rewards are exposed to repeated harvesting through reversible pair interactions
- AMM reserve assumptions break when taxed transfers are pushed into a pair before
sync()
Victim token code excerpt:
address public uniswapV2Pair;
address private _baseToken = 0x55d398326f99059fF775485246999027B3197955;
constructor(TokenWarp _warp) ERC20(_name, _symbol) {
address _uniswapV2Pair =
IUniswapV2Factory(_uniswapV2Router.factory()).createPair(address(this), address(_baseToken));
uniswapV2Pair = _uniswapV2Pair;
payTokenAddress = 0x8eBeb2bf3C6CA7d5C6b345515e368E94Eda0aB26;
}
function bindInviter(address from, address to, uint256 amount) internal {
if (inviter[to] == address(0) && amount == 10**15) beinvited[to] = from;
if (inviter[from] == address(0) && beinvited[from] == to && amount == 5 * 10**14) {
inviter[to] = from;
}
}
function takeAllFee(address from, address to, uint256 amount) private returns (uint256 amountAfter) {
if (to == uniswapV2Pair) {
transferToken(from, payTokenAddress, amount * 1 / 100);
transferToken(from, address(this), amount * 14 / 100);
promoteReward(from, amount);
} else {
transferToken(from, payTokenAddress, amount * 50 / 100);
}
}
That excerpt is sufficient to explain the invariant break: a sell should only pay referral rewards from value actually consumed by the sell, and all live BCT pairs should be handled consistently. BCT violates both conditions.
4. Detailed Root Cause Analysis
4.1 Pair misclassification creates exploitable treasury inventory
The verified BCT source fixes uniswapV2Pair to the constructor-created USDT pair. The external BCT/WBNB pair is therefore invisible to the token's pair-specific logic. Whenever BCT moves through BCT/WBNB, _transfer falls through to the generic branch in takeAllFee and moves 50% of the amount into payTokenAddress. The exploit balance diff for tx 0xdae0... shows that this treasury was already heavily funded before the attack and lost 1130499359007577072673681 raw BCT units during the exploit.
The ACT exploit conditions were all public and satisfied on-chain before the exploit:
- the attacker could deploy arbitrary helper contracts
- the inviter chain could be created with the public
1e15/5e14transfer pattern - each inviter could be topped above 30 BCT using public BCT/WBNB liquidity
payTokenAddressalready held enough BCT inventory to fund repeated rewards- the BCT/USDT pair still held USDT and the BCT/WBNB pair still held WBNB for exit liquidity
4.2 Referral binding is public and deterministic
The attacker first sent a tiny setup transaction to the primary attacker contract 0xe9616ff20ad519bce0e3d61353a37232f0c27a50, passing five helper addresses. The setup trace shows the exact 1e15 / 5e14 pattern required by bindInviter:
emit Transfer(from: 0xe9616f..., to: 0xcc1378..., value: 500000000000000)
Token::transfer(0xe9616f..., 500000000000000)
emit Transfer(from: 0xcc1378..., to: 0xe9616f..., value: 250000000000000)
...
emit Transfer(from: 0x64b200..., to: 0x3b132c..., value: 500000000000000)
Token::transfer(0x64b200..., 500000000000000)
emit Transfer(from: 0x3b132c..., to: 0x64b200..., value: 250000000000000)
Those transfers correspond to the attacker-controlled chain:
- attacker contract
0xe9616f... - helper1
0xcc1378... - helper2
0xf5eb1f... - helper3
0x23d167... - helper4
0x64b200... - helper5
0x3b132c...
The setup balance diff confirms that each helper ended the transaction with a small positive BCT balance, and the exploit transaction later tops each helper up above the 30 BCT threshold.
4.3 Flash liquidity qualifies inviters and starts the drain loop
In tx 0xdae0..., the attacker contract flash-borrowed 20 WBNB from the WBNB/BUSD pair. The exploit trace shows pancakeCall, then five buys from the BCT/WBNB pair that each deliver 60 BCT gross to a helper. That satisfies the balanceOf(inviter) >= 30 * 10**18 gate in promoteReward.
Immediately after qualification, the attacker starts the core loop:
- Move WBNB through BCT/WBNB so BCT arrives at the hardcoded BCT/USDT pair.
- Call
skim()on BCT/USDT to reclaim the excess BCT balance that now sits above reserves. - Transfer that recovered BCT back into the same pair, which triggers
promoteReward(from, amount)becauseto == uniswapV2Pair. - Receive referral payouts from
payTokenAddressacross the five inviter levels. - Dump helper excess balances into the same pair and continue.
The first drain cycle is explicit in the trace:
PancakePair::skim(0xe9616f...)
Token::transfer(0xe9616f..., 194045828222693844822)
emit Transfer(from: 0x8eBeb2..., to: 0xcc1378..., value: 29106874233404076723)
emit Transfer(from: 0x8eBeb2..., to: 0xf5Eb1F..., value: 19404582822269384482)
emit Transfer(from: 0x8eBeb2..., to: 0x23D167..., value: 9702291411134692241)
emit Transfer(from: 0x8eBeb2..., to: 0x64B200..., value: 9702291411134692241)
emit Transfer(from: 0x8eBeb2..., to: 0x3B132C..., value: 9702291411134692241)
emit Transfer(from: PancakePair: [0x5A25...], to: 0xe9616f..., value: 164938953989289768099)
The exploit trace contains 278 skim() calls, which matches the root-cause claim that the attacker repeatedly recycled reclaimable principal into new treasury-funded rewards.
4.4 Conversion and exit
After materially draining payTokenAddress, the attacker used the remaining USDT and BCT flows to re-enter the BCT/WBNB pair, converted the recovered inventory back into WBNB, repaid the flash swap with fee, and sent the surplus to the profit EOA 0x9c66b0c68c144ffe33e7084fe8ce36ebc44ad21e.
End-of-trace excerpt:
WBNB::transfer(0x1B96B92314C44b159149f7E0303511fB2Fc4774f, 20050000000000000001)
WBNB::balanceOf(0xe9616f...) -> 10154043154622408863
WBNB::transfer(0x9c66B0c68c144Ffe33E7084FE8cE36EBC44aD21e, 10154043154622408863)
That final transfer amount matches the profit figure reported in root_cause.json.
5. Adversary Flow Analysis
The attack is a clean two-transaction ACT sequence.
0xd4c19d575ea5b3a415cc288ce09942299ca3a3b49ef9718cda17e4033dd4c250The EOA0x9c66b0c68c144ffe33e7084fe8ce36ebc44ad21esends0.00065BNB to the attacker contract0xe9616f..., passing five helper addresses. The contract buys dust BCT, executes the1e15/5e14transfer handshake five times, and builds the inviter ladder.0xdae0b85e01670e6b6b317657a72fb560fc388664cf8bfdd9e1b0ae88e0679103The same EOA calls the same attacker contract. The contract flash-borrows 20 WBNB, buys enough BCT for each helper to cross the 30 BCT gate, repeatedly harvestspayTokenAddressby selling into the hardcoded BCT/USDT pair and reclaiming principal withskim(), dumps helper balances through the same pair, converts the resulting inventory via BCT/WBNB, repays20.05WBNB to the flash pair, and transfers the remaining WBNB to the EOA.
The decision points are fully public and permissionless:
- The attacker can deploy new helper contracts.
- The inviter logic depends only on public transfer sizes.
- The flash swap uses public PancakeSwap liquidity.
- The profit path uses public AMM reserves and public pair mechanics.
No privileged key, whitelist, or private infrastructure is required.
6. Impact & Losses
The measurable impacts are:
payTokenAddresslost1130499359007577072673681raw BCT units.- The attacker EOA received
10154043154622408863raw WBNB units after flash-loan repayment. - The BCT/USDT and BCT/WBNB pools were used as exit liquidity, so external liquidity providers absorbed part of the economic loss while the protocol treasury was directly depleted.
The setup transaction cost the attacker 4291523000000000 wei in gas-equivalent native value. The exploit transaction cost 89147673000000000 wei in native value. Those costs do not offset the positive WBNB result shown in the exploit trace and balance diff.
7. References
- Tx
0xd4c19d575ea5b3a415cc288ce09942299ca3a3b49ef9718cda17e4033dd4c250metadata: metadata.json - Tx
0xd4c19d575ea5b3a415cc288ce09942299ca3a3b49ef9718cda17e4033dd4c250trace: trace.cast.log - Tx
0xd4c19d575ea5b3a415cc288ce09942299ca3a3b49ef9718cda17e4033dd4c250balance diff: balance_diff.json - Tx
0xdae0b85e01670e6b6b317657a72fb560fc388664cf8bfdd9e1b0ae88e0679103metadata: metadata.json - Tx
0xdae0b85e01670e6b6b317657a72fb560fc388664cf8bfdd9e1b0ae88e0679103trace: trace.cast.log - Tx
0xdae0b85e01670e6b6b317657a72fb560fc388664cf8bfdd9e1b0ae88e0679103balance diff: balance_diff.json - Verified BCT source: Contract.sol