SkyDog Honeypot

Reported loss Not disclosed
BNB Chain
Honeypot

What happened

SkyDog (SDOGE) at 0xc0ea5587…ae2483 is a BNB Smart Chain token whose transfer hook, burnTokenCheck, lets the owner, one owner-chosen address (_safeOwner), anyone sending to the owner, and owner-whitelisted wallets send tokens without restriction. A holder outside those groups can only send to the hard-coded _unirouter address, so a sell into the liquidity pair, whose recipient is the pair rather than _unirouter, reverts with "ERC20: transfer amount exceeds balance". The owner controls the whitelist and blacklist and can also mint to their own balance.

De.Fi's REKT database lists the token as an exit scam/honeypot in which the owner could disable the transfer function and stop users from selling.

How it happened

  1. The constructor stores the deployer in both _owner and _safeOwner and mints the initial supply to the owner. Every transfer, including those made through transferFrom, runs through _approveCheck, which carries the burnTokenCheck(sender,recipient,amount) modifier.
  2. In the modifier, the first branch is if (_owner == _safeOwner && sender == _owner){_safeOwner = recipient;_;}. The first transfer sent by the owner therefore moves _safeOwner to that transfer's recipient. If that transfer is the liquidity deposit made through a router, the recipient is the pair, so the pair becomes _safeOwner; the source does not show which address it ended up as.
  3. Next branch: if (sender == _owner || sender == _safeOwner || recipient == _owner) lets the call through unconditionally. So the owner, the _safeOwner address, and anyone sending to the owner are never blocked, and _sellAmount is set only when the owner sends to themselves.
  4. Every other sender falls to the last part. A whitelisted sender (_whiteAddress) passes. A blacklisted sender must satisfy require((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance"), which for a blacklisted holder means only transfers to the hard-coded _unirouter address succeed.
  5. A holder who is on neither list hits if (amount < _sellAmount){ ... }else{require((sender == _safeOwner)||(recipient == _unirouter), ...)}. _sellAmount starts at 0, so amount < _sellAmount is false and the require applies. A router sell calls transferFrom(holder, pair, amount) with the pair as recipient, which is neither _unirouter nor a sender equal to _safeOwner, so it reverts before _balances[sender] is reduced. Plain wallet-to-wallet transfers by such holders fail the same way.
  6. If the owner sets _sellAmount (by sending an amount to themselves), a sell smaller than that amount goes through but runs if(recipient == _safeOwner){_blackAddress[sender] = true; _whiteAddress[sender] = false;}, which blacklists the seller when the recipient is _safeOwner; sells at or above that amount revert.
  7. The owner decides who is exempt: increaseAllowance(address[] receivers) sets _whiteAddress = true and clears the blacklist, addApprove(address[] receivers) blacklists, decreaseAllowance(address safeOwner) replaces _safeOwner, and multiTransfer(approvecount, receivers, amounts) whitelists the first approvecount recipients and sets their allowance to the router to the maximum. _mint(address account, uint256 amount) is public and only checks msg.sender == _owner; it adds amount to _balances[_owner] and to _totalSupply. There is no ownership transfer or renounce function. No transaction showing the owner calling any of these was opened, so the source shows what the owner could do, not what was done.

Protocol details

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

Build your security review skills

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