Vitalik Shiba Buterin Honeypot
What happened
Vitalik Shiba Buterin (VSHIBA) is an Ethereum ERC-20 whose transfer and transferFrom both pass through a modifier named burnTokenCheck. For a sender on neither the owner's whitelist nor its blacklist, the modifier allows a transfer only if the recipient is the owner or the hard-coded _unirouter address, unless the owner has set a positive _sellAmount. The source does not state what _unirouter is.
A holder's sell into a trading pair is a transfer from the holder to the pair, so for such a holder it can revert with "ERC20: transfer amount exceeds balance" even though the balance is sufficient. The owner functions increaseAllowance, addApprove and decreaseAllowance whitelist, blacklist and set _safeOwner, but the burnTokenCheck modifier also changes this state during transfers: it sets _safeOwner = recipient on the first transfer whose sender is the owner, and it blacklists a sender on neither list who sends below _sellAmount to _safeOwner. The owner can also mint through a public _mint.
De.Fi's REKT database lists VSHIBA as an Exit Scam/Honeypot dated 2021-05-12 and says the contract owner could disable the transfer function, which restricted users in selling their tokens. The contract was deployed on 2021-05-12 at 20:00:39 UTC by 0x12cCf4B7…1a88f3 in transaction 0xa143cd2d…d7adf1.
How it happened
transferandtransferFromboth call_approveCheck(...), which carries the modifierburnTokenCheck(sender,recipient,amount). The check looks at thesenderandrecipientarguments, not atmsg.sender, so a transfer is judged by the account the tokens leave and the account they go to.- Senders that pass: the first branch
if (_owner == _safeOwner && sender == _owner){_safeOwner = recipient;_;}lets the owner's first transfer through and makes its recipient the new_safeOwner. After that,if (sender == _owner || sender == _safeOwner || recipient == _owner)lets the owner,_safeOwner, and anyone sending to the owner through;if (_whiteAddress[sender] == true){ _; }lets whitelisted senders through. - Everyone else is neutral, meaning on neither list. While
_sellAmountis at its initial0,if (amount < _sellAmount)is never true, so the code reachesrequire((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance");. A neutral holder can therefore send tokens only to the hard-coded_unirouteraddress (or the owner); a transfer to any other recipient, such as a pair address, reverts, with the holder's balance untouched. Whether buys from a pair work depends on the pair being_safeOwneror whitelisted, which the code alone does not show, and the source does not say what_unirouteris. - The owner can set
_sellAmountby callingtransferto itself:if (sender == _owner && sender == recipient){_sellAmount = amount;}. This works only once_safeOwneris no longer the owner, because while_owner == _safeOwnerthe first branch handles every transfer from the owner and a self-transfer just sets_safeOwner = recipient. With_sellAmountabove zero, a neutral sender moving less than that amount passes, andif(recipient == _safeOwner){_blackAddress[sender] = true; _whiteAddress[sender] = false;}blacklists that sender when the recipient is_safeOwner. A blacklisted sender can then send only to_unirouteror the owner:if (_blackAddress[sender] == true){ require((sender == _safeOwner)||(recipient == _unirouter), ...). - Owner-only functions, each guarded by
require(msg.sender == _owner, "!owner"):multiTransferwhitelists the firstapprovecountreceivers and approves the_unirouteraddress for the maximum amount on their behalf;increaseAllowance(address[])whitelists and un-blacklists;addApprove(address[])blacklists and un-whitelists;decreaseAllowance(address safeOwner)sets_safeOwner. Despite their ERC-20-style names, none of these change an allowance for a normal holder. The modifier is a second path: any transfer whose sender is the owner (including atransferFromspending the owner's allowance) can reassign_safeOwnerwhile_owner == _safeOwner, and the auto-blacklist in step 4 needs no owner call. The contract has no function that changes_owner. _mintispublicwith only an owner check and credits_balances[_owner] = _balances[_owner].add(amount)while emittingTransfer(address(0), account, amount)for theaccountargument. The constructor calls it 30 times (1 for_owner, then 9, 10 and 10 for three hard-coded addresses), so the events show tokens going to other addresses while the balances credit_owner. The owner can call_mintagain at any time.
Protocol details
Build your security review skills
Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.