GrumpyDoge Honeypot

Reported loss Not disclosed
Ethereum
Honeypot

What happened

GrumpyDoge (GrumpDoge) is an Ethereum token whose transfer logic runs through a burnTokenCheck modifier controlled by its owner. Unless the sender is the owner, the address stored as _safeOwner, or whitelisted by the owner, a transfer is allowed only if the recipient is the hard-coded _unirouter address or the owner. While _sellAmount is 0, which is its initial value, a sell into a pair can therefore revert with "ERC20: transfer amount exceeds balance" even when the holder has the tokens.

Once the owner has set a positive _sellAmount, a sender moving less than that amount passes instead. De.Fi's REKT database lists the token as an exit scam/honeypot, saying the contract owner could restrict users from selling.

How it happened

  1. The constructor sets _owner and _safeOwner to the deployer and calls _mint(0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B, initialSupply*(10**18)). _mint credits _balances[_owner] rather than the account argument, so the 100,000,000,000 tokens start with the deployer even though the Transfer event names the other address.
  2. Both transfer and transferFrom call _approveCheck, which is wrapped in burnTokenCheck(sender,recipient,amount). While _owner == _safeOwner, a transfer from the owner passes and moves _safeOwner to its recipient: if (_owner == _safeOwner && sender == _owner){_safeOwner = recipient;_;}.
  3. After that, transfers pass freely if the sender is _owner or _safeOwner, or if the recipient is _owner. A whitelisted sender also passes. A blacklisted sender passes only if sender == _safeOwner or the recipient is _unirouter.
  4. Every other sender falls to if (amount < _sellAmount) {...} else { require((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance"); }. _sellAmount starts at 0, so amount < _sellAmount is false and, while it stays 0, such a sender can only send to _unirouter (or to _owner under step 3). A sale that transfers tokens to a pair address can then revert unless that sender is whitelisted. If _sellAmount becomes positive, transfers below it pass.
  5. The owner controls the lists and thresholds: increaseAllowance(address[]) whitelists addresses, addApprove(address[]) blacklists them, and decreaseAllowance(address) sets _safeOwner. The owner can also set _sellAmount, but only after _safeOwner has moved off the owner: the line if (sender == _owner && sender == recipient){_sellAmount = amount;} is reached only when _owner != _safeOwner, because while _owner == _safeOwner the first branch runs instead and a self-transfer just sets _safeOwner = recipient. Once _sellAmount is above 0, a non-privileged sender sending less than it passes, and if the recipient is _safeOwner the sender is added to _blackAddress.
  6. _mint is public and only checks msg.sender == _owner, so the code lets the owner mint further supply into the owner's own balance. There is no function that renounces or transfers _owner.

Protocol details

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

Build your security review skills

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