Dasani Honeypot
What happened
Dasani (DASANI) is a BNB Smart Chain token whose transfers all pass through a burnTokenCheck modifier. Once the owner has made a first outgoing transfer, any holder who is not the owner, not the stored _safeOwner and not whitelisted can only send tokens to the hard-coded _unirouter address 0x05fF2B0D…608C7F or to the owner, unless the amount is below _sellAmount, which is 0 until the owner sets it. A sell that ends in a pool address is therefore rejected, with an error message that talks about balance although the holder's balance is untouched.
The owner can also whitelist, blacklist and re-point _safeOwner, and can mint more tokens. De.Fi's REKT database lists the token as an exit scam/honeypot and says the contract owner could disable the transfer function, which restricted users in selling their tokens. No transaction or holder data was opened for this token, so the code shows what the owner could do, not what was done.
How it happened
- The constructor sets the owner and mints the whole supply to it:
_owner = owner; _safeOwner = owner; _mint(_owner, initialSupply*(10**18));. Nothing in the contract changes_ownerafterwards. - Every
transferandtransferFromruns_approveCheck(...), which carries theburnTokenCheckmodifier. Its first branch isif (_owner == _safeOwner && sender == _owner){_safeOwner = recipient;_;}, so the recipient of the owner's first outgoing transfer becomes_safeOwner. - Owner and
_safeOwneras senders, and anyone sending to the owner, pass:if (sender == _owner || sender == _safeOwner || recipient == _owner){. - Every other sender who is not whitelisted reaches
require((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance");. Since such a sender is never_safeOwnerhere, the transfer only succeeds when the recipient is_unirouter. A sell that moves tokens into a pool pair throughtransferFromhas the pair as recipient and reverts. The balance is only debited after the modifier passes, so a revert leaves the holder's balance unchanged. A buy, where the pair is the sender, passes only if the pair is the owner,_safeOwneror whitelisted;_safeOwneris whatever the owner's first outgoing transfer was sent to. - The one gap is
if (amount < _sellAmount)._sellAmountstarts at 0 (uint256 private _sellAmount = 0;) and is only set when the owner transfers to itself:if (sender == _owner && sender == recipient){_sellAmount = amount;}. When a smaller transfer does pass and its recipient is_safeOwner, the sender is blacklisted:if(recipient == _safeOwner){_blackAddress[sender] = true; _whiteAddress[sender] = false;}. A blacklisted sender is held to the same recipient rule as step 4. - The owner controls the lists.
increaseAllowance(address[] memory receivers)whitelists addresses (_whiteAddress[receivers[i]] = true;),addApproveblacklists them,decreaseAllowance(address safeOwner)replaces_safeOwner, andmultiTransferwhitelists the firstapprovecountreceivers and approves the maximum amount to_unirouterfor them. The function names do not describe these effects. A whitelisted sender passes the modifier with no recipient restriction. _mintispublicbut owner-only and credits the owner, not itsaccountargument:require(msg.sender == _owner, "ERC20: mint to the zero address"); ... _balances[_owner] = _balances[_owner].add(amount);. The code places no cap on further minting by the owner.
Protocol details
Evidence
Build your security review skills
Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.