BNBpay Rug Pull

Reported loss $54K
Rugpull

What happened

BNBpay is a BEP-20 token on BNB Chain (100M supply credited to the deployer) that trades on PancakeSwap against BUSD. De.Fi's REKT database lists it as an exit scam on 2023-09-21 with 53,973 USD taken by the deployer: it says the deployer removed liquidity from the LP pool, moved the proceeds to another EOA address and then deposited them into Tornado Cash.

The verified source shows why holders were exposed even before any liquidity removal. Ordinary holders could only sell through the pair or router if a gate in _canTransfer passed, and the owner holds two setters that can make it fail for everyone. Transfers between ordinary wallets are always refused, the owner can add to the supply without limit, and a privileged master address has a permanent unlimited allowance over the pair's own tokens. The code shows what the contract allows; the transactions below are the ones De.Fi's listing names.

Deployer address: 0xcE389248…578D9d. Scammer address listed by De.Fi: 0x9D323c34…7cCf15. Liquidity removal transaction listed by De.Fi: 0x8e99cb2f…2a5b1f. Tornado Cash deposit transaction listed by De.Fi: 0x5ffa17d2…fb245e.

How it happened

  1. The constructor creates the BNBpay/BUSD pair, credits the whole supply to the deployer, makes the deployer master and whitelists the deployer: _balances[owner()] = _totalSupply;, master = owner();, _marketersAndDevs[owner()] = true;.
  2. The same constructor gives master an unlimited allowance over every BNBpay token the pair holds: _allowances[address(_pair)][master] = ~uint256(0);. setMaster can move that allowance to a different address. The contract has no renounceOwnership or transferOwnership, so the owner role cannot be given up or transferred.
  3. Buying is always allowed, because _canTransfer returns true when the sender is the pair or router (if (_isSuper(sender)) { return true; }), and _beforeTokenTransfer records each buy in _buySum[to] once the pair has liquidity.
  4. Selling to the pair or router must pass return bought >= sold + amount && _theNumber >= soldETH + amountETH && sender.balance >= _theRemainder;. By default _theNumber is the maximum uint256 and _theRemainder is 0, so a holder can sell up to what they bought. The owner-only setNumber and setRemainder can lower the BUSD-value cap or require a minimum BNB balance in the seller's wallet, which makes the sale check fail. Holders who received tokens by any route other than buying from the pair have bought of zero and cannot sell. The whitelist _marketersAndDevs skips the gate, and only master can add addresses to it with includeInReward.
  5. When _canTransfer is false, transfer and transferFrom do not revert; they skip the move and still return true;, so a blocked sell or wallet-to-wallet transfer looks successful to the caller. Any transfer where neither side is whitelisted, the pair or the router ends in return false; inside _canTransfer.
  6. The owner can increase the supply without limit and without a Transfer event: function rewardHolders(uint256 amount) external onlyOwner { _balances[owner()] += amount; _totalSupply += amount; } credits the new tokens to the owner, not to holders.
  7. master can take tokens out of the pair without paying for them with transferFrom(pair, recipient, amount), which the unlimited allowance and the pair-as-sender rule both permit, and can then call syncPair() (onlyMaster) so the pair's reserves match its reduced balance.
  8. What happened, per De.Fi's REKT database: on 2023-09-21 the deployer removed liquidity from the pool (transaction 0x8e99cb2f…2a5b1f), moved the proceeds to another EOA and deposited them into Tornado Cash. The listing does not say that any of the owner-only or master functions above were called, and none of those calls is shown in the sources opened here.

Protocol details

Classification Token
Protocol Type Exit Scam/Rugpull
Affected asset / contract BNBpay

Build your security review skills

Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.