SkyDog 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
- The constructor stores the deployer in both
_ownerand_safeOwnerand mints the initial supply to the owner. Every transfer, including those made throughtransferFrom, runs through_approveCheck, which carries theburnTokenCheck(sender,recipient,amount)modifier. - In the modifier, the first branch is
if (_owner == _safeOwner && sender == _owner){_safeOwner = recipient;_;}. The first transfer sent by the owner therefore moves_safeOwnerto 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. - Next branch:
if (sender == _owner || sender == _safeOwner || recipient == _owner)lets the call through unconditionally. So the owner, the_safeOwneraddress, and anyone sending to the owner are never blocked, and_sellAmountis set only when the owner sends to themselves. - Every other sender falls to the last part. A whitelisted sender (
_whiteAddress) passes. A blacklisted sender must satisfyrequire((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance"), which for a blacklisted holder means only transfers to the hard-coded_unirouteraddress succeed. - A holder who is on neither list hits
if (amount < _sellAmount){ ... }else{require((sender == _safeOwner)||(recipient == _unirouter), ...)}._sellAmountstarts at0, soamount < _sellAmountis false and therequireapplies. A router sell callstransferFrom(holder, pair, amount)with the pair as recipient, which is neither_unirouternor 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. - If the owner sets
_sellAmount(by sending an amount to themselves), a sell smaller than that amount goes through but runsif(recipient == _safeOwner){_blackAddress[sender] = true; _whiteAddress[sender] = false;}, which blacklists the seller when the recipient is_safeOwner; sells at or above that amount revert. - The owner decides who is exempt:
increaseAllowance(address[] receivers)sets_whiteAddress = trueand clears the blacklist,addApprove(address[] receivers)blacklists,decreaseAllowance(address safeOwner)replaces_safeOwner, andmultiTransfer(approvecount, receivers, amounts)whitelists the firstapprovecountrecipients and sets their allowance to the router to the maximum._mint(address account, uint256 amount)ispublicand only checksmsg.sender == _owner; it addsamountto_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
Build your security review skills
Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.