0xdebaa13fb06134e63879ca6bcb08c5e0290bdbac3acf67914c0b1dcaf0bdc3dd0x09762e00ce0de8211f7002f70759447b1f2b1892BSC0x02e8ead6de82c8a248ef0eebe145295116d0e4c2BSC0x6b7e9be56ca035d3471da76caa99f165449697a0BSC0xba0d236fbcbd34052cdab29c4900063f9efe6e4fBSCTipTag Pump was exploited on BSC in transaction 0xdebaa13fb06134e63879ca6bcb08c5e0290bdbac3acf67914c0b1dcaf0bdc3dd at block 47169116. A permissionless adversary used a public Pancake V3 flash loan to manipulate four fresh TipTag Pump token listings in one transaction. For each token, the attacker first sent bonding-curve output directly into the token's pre-created Pancake pair, then added 1 WBNB and minted LP before the protocol listing code ran. When the final buy triggered Token._makeLiquidityPool(), TipTag Pump seeded liquidity against the attacker-controlled reserves instead of a clean empty pair, after which the attacker dumped the freshly acquired tokens and drained nearly all WBNB from each pool.
The root cause is a protocol-side listing invariant failure. Token.buyToken() accepts any nonzero receiver while the token is still unlisted, _beforeTokenTransfer() explicitly allows address(this) -> pair transfers before listing, and initialize() creates the pair early enough that its address is already known. Token._makeLiquidityPool() then trusts the pair state and calls PancakeRouter.addLiquidityETH without verifying that the pair still has zero reserves and zero third-party LP supply.
TipTag Pump token launches use a bonding-curve phase before a PancakeSwap listing. During initialize(), each token mints 650,000,000 tokens for the bonding curve and tokens for future DEX liquidity, then immediately creates a Pancake V2-style pair against WBNB. At that stage the token remains unlisted, but the pair address is already live and discoverable on-chain.
200,000,000Users buy bonding-curve inventory through Token.buyToken(). If the purchase crosses bondingCurveTotalAmount, the function transfers the remaining bonding-curve tokens to the chosen receiver and then calls _makeLiquidityPool(). That listing function approves the router and calls addLiquidityETH with the token contract's full native balance plus the fixed 200,000,000 token liquidity tranche. The intended model is that the protocol seeds a fresh pool and determines the initial token/WBNB ratio itself.
The critical assumption is that the pair is still clean when listing occurs. That assumption is false because the contract lets the token contract itself transfer pre-listing output into the pair, and the pair already exists before listing.
This is an ATTACK-class root cause, not benign arbitrage. TipTag Pump exposes a public path that lets any unprivileged actor corrupt the initial liquidity state before listing. The vulnerable sequence starts when initialize() creates the pair while listed == false, making the future liquidity endpoint public. buyToken() only rewrites receiver when it is zero, so an attacker can deliberately choose the pair as the pre-listing recipient. _beforeTokenTransfer() blocks unlisted transfers into pair only when from != address(this), which means the token contract may still send bonding-curve inventory directly into the pair. Once the attacker also contributes a small amount of WBNB and mints LP, the pair reserves no longer reflect a clean protocol-controlled listing. When _makeLiquidityPool() later calls addLiquidityETH, the router preserves the attacker-defined reserve ratio, causing the protocol to contribute roughly 20 WBNB per pair at a manipulated price and enabling the attacker to dump into that liquidity for profit.
The first breakpoint is in the verified Token.sol implementation. initialize() creates the pair before the token is listed:
_mint(address(this), bondingCurveTotalAmount + liquidityAmount);
_mint(address(manager), socialDistributionAmount);
pair = factory.createPair(address(this), IPump(manager).getWETH());
The second breakpoint is the pre-listing receiver handling in buyToken():
if (receiver == address(0)) {
receiver = tx.origin;
}
...
this.transfer(receiver, tokenReceived);
The third breakpoint is the transfer hook:
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
if (!listed && to == pair && from != address(this)) {
revert TokenNotListed();
}
return super._beforeTokenTransfer(from, to, amount);
}
Taken together, these paths let the token contract itself move bonding-curve output into the pair before listing, even though other users cannot transfer into the pair while unlisted. That violates the correct invariant: before _makeLiquidityPool() runs, the pair should have zero reserves and zero third-party LP so the protocol alone sets the initial price.
The seed trace shows the invariant break in concrete on-chain order. The public flash pool 0x172fcd41e0913e95784454622d1c3724f546f849 sends 100 WBNB, then calls the attack helper callback. Inside that callback, the helper calls Token::buyToken{value: 0.001 ether}(..., receiver = pair), transfers 1 WBNB into the pair, and calls PancakePair::mint(attacker) before the final listing-triggering buy:
0x172fcd41e0913e95784454622d1c3724f546f849::flash(..., 100000000000000000000)
0x0E220c6c52d383869A5085Ef074b6028254b3462::pancakeV3FlashCallback(...)
Token::buyToken{value: 1000000000000000}(..., PancakePair: [0xE3020449775B134d43Dba0816F4bEd60B8F4e193])
WBNB::transfer(PancakePair: [0xE3020449775B134d43Dba0816F4bEd60B8F4e193], 1000000000000000000)
PancakePair::mint(0x0E220c6c52d383869A5085Ef074b6028254b3462)
The same trace then shows the manipulated listing on the already-seeded pair. buyToken{value: 20 ether} triggers _makeLiquidityPool(), and the router observes nonzero reserves before adding protocol liquidity:
Token::buyToken{value: 20000000000000000000}(..., 0x0E220c6c52d383869A5085Ef074b6028254b3462)
PancakeRouter::addLiquidityETH{value: 20000000339918964164}(
Token: [0x09762e00Ce0DE8211F7002F70759447B1F2b1892],
200000000000000000000000000,
...
)
PancakePair::getReserves() -> 54458542087245239876758, 1000000000000000000
emit TokenListedToDex(pair: PancakePair: [0xE3020449775B134d43Dba0816F4bEd60B8F4e193])
At that point the router is no longer initializing a clean pool. It is matching the attacker's reserve ratio, so the protocol adds a large amount of WBNB while only a comparatively small token amount is required to preserve the attacker-imposed price. The helper then swaps its freshly obtained token balance back to WBNB, draining almost all WBNB from the listed pool. The trace repeats this same sequence for the other three TipTag tokens and pairs in the same transaction.
The adversary cluster contains EOA 0x5d6e908c4cd6eda1c2a9010d1971c7d62bdb5cd3 and helper contract 0x0e220c6c52d383869a5085ef074b6028254b3462. The EOA submits the transaction and ultimately receives the residual native profit. The helper contract executes the exploit logic, holds the flash-loaned WBNB, pre-seeds the pairs, receives LP, performs the swaps, repays the flash loan, and unwraps any remaining WBNB.
The execution flow is deterministic and fully permissionless:
100 WBNB from the public Pancake V3 pool via flash.buyToken with receiver = pair to place bonding-curve output directly into the pair;1 WBNB into the pair;buyToken call that crosses the listing threshold and triggers _makeLiquidityPool();0.01 WBNB fee.The balance diff corroborates the economic result. The sender EOA 0x5d6e... moves from 0.080195846886051890 BNB to 11.359212309937418427 BNB, a net delta of 11.279016463051366537 BNB, while the four victim listing pools collectively lose 83.750748300501254811 WBNB.
The direct asset loss is 83.750748300501254811 WBNB extracted from the four manipulated TipTag listing pools. The exploited pools correspond to the TipTag token contracts:
0x09762e00ce0de8211f7002f70759447b1f2b18920x02e8ead6de82c8a248ef0eebe145295116d0e4c20x6b7e9be56ca035d3471da76caa99f165449697a00xba0d236fbcbd34052cdab29c4900063f9efe6e4fAfter flash-loan repayment and fee settlement, the attacker retains 11.279016463051366537 BNB in the transaction sender EOA. The incident therefore satisfies the ACT success predicate: a permissionless adversary can realize material net profit using only public state, public contracts, and a public flash-loan source.
0xdebaa13fb06134e63879ca6bcb08c5e0290bdbac3acf67914c0b1dcaf0bdc3dd on BSC block 47169116.Token.sol, especially initialize(), buyToken(), _makeLiquidityPool(), and _beforeTokenTransfer().