Capellix Honeypot

Reported loss Not disclosed
BNB Chain
Honeypot

What happened

Capellix (CPLX) is a BEP20 token on BNB Smart Chain whose transfer and transferFrom are wrapped in modifiers that revert any transfer to a stored address, prevOwner, while the ownerLock flag is on. The flag is set to true in the constructor and is changed with setOwnerLock, which is guarded by onlyOwner; that modifier passes when _owner == _msgSender() and also when _owner == prevOwner. The first transferFrom made while prevOwner is empty stores that call's recipient as prevOwner, and transferOwnership (also onlyOwner) overwrites it without changing _owner.

De.Fi's REKT database lists Capellix as an Exit Scam/Honeypot in which the contract owner could disable transfers and so restrict holders from selling.

How it happened

  1. The constructor calls Initialize(), which sets ownerLock=true and credits the whole supply (_maxSupply = 1 * 10**8 * 10**9) to the deployer.
  2. transfer carries transferMiddleware(recipient,ownerLock). While ownerLock is true it runs require(to != prevOwner, "Handling Request"), so any direct transfer to prevOwner reverts.
  3. transferFrom carries transferFromMiddleware(sender, recipient, ownerLock). While ownerLock is true: if(from != address(0) && prevOwner == address(0)) prevOwner = to; else require(to != prevOwner, "Order TransferFromMiddleware");. The first transferFrom therefore stores its recipient in prevOwner, and every later transferFrom to that address reverts. The code does not show which address that first recipient was.
  4. If prevOwner is the trading pair, a sell that moves tokens to the pair reverts, while other transfers and buys are not touched by this check.
  5. onlyOwner is require((_owner == _msgSender() || _owner == prevOwner), "Ownable: caller is not the owner");. The owner passes it. So does anyone else whenever _owner == prevOwner, for example if the first captured recipient was the owner's own address. transferOwnership(address newOwner) only runs prevOwner = newOwner and does not change _owner, so an owner can point prevOwner at an unused address to lift the block, or at address(0) so the next transferFrom re-captures a recipient. setOwnerLock(bool) switches the check off or back on.
  6. renounceOwnership() sets _owner = address(0). After that, onlyOwner passes for every caller if prevOwner is still address(0), because _owner == prevOwner then holds, so anyone could call setOwnerLock and transferOwnership. If prevOwner holds another address, no caller passes. The code alone does not show whether or when any of these functions was called; De.Fi lists the token as a honeypot.

Protocol details

Classification Token
Protocol Type Exit Scam/Honeypot
Affected asset / contract CPLX

Build your security review skills

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