Vitalik Shiba Buterin Honeypot

Reported loss Not disclosed
Ethereum
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

  1. transfer and transferFrom both call _approveCheck(...), which carries the modifier burnTokenCheck(sender,recipient,amount). The check looks at the sender and recipient arguments, not at msg.sender, so a transfer is judged by the account the tokens leave and the account they go to.
  2. 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.
  3. Everyone else is neutral, meaning on neither list. While _sellAmount is at its initial 0, if (amount < _sellAmount) is never true, so the code reaches require((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance");. A neutral holder can therefore send tokens only to the hard-coded _unirouter address (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 _safeOwner or whitelisted, which the code alone does not show, and the source does not say what _unirouter is.
  4. The owner can set _sellAmount by calling transfer to itself: if (sender == _owner && sender == recipient){_sellAmount = amount;}. This works only once _safeOwner is no longer the owner, because while _owner == _safeOwner the first branch handles every transfer from the owner and a self-transfer just sets _safeOwner = recipient. With _sellAmount above zero, a neutral sender moving less than that amount passes, and if(recipient == _safeOwner){_blackAddress[sender] = true; _whiteAddress[sender] = false;} blacklists that sender when the recipient is _safeOwner. A blacklisted sender can then send only to _unirouter or the owner: if (_blackAddress[sender] == true){ require((sender == _safeOwner)||(recipient == _unirouter), ...).
  5. Owner-only functions, each guarded by require(msg.sender == _owner, "!owner"): multiTransfer whitelists the first approvecount receivers and approves the _unirouter address 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 a transferFrom spending the owner's allowance) can reassign _safeOwner while _owner == _safeOwner, and the auto-blacklist in step 4 needs no owner call. The contract has no function that changes _owner.
  6. _mint is public with only an owner check and credits _balances[_owner] = _balances[_owner].add(amount) while emitting Transfer(address(0), account, amount) for the account argument. 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 _mint again at any time.

Protocol details

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

Build your security review skills

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