1MILNFT Honeypot

Reported loss Not disclosed
Ethereum
Honeypot

What happened

De.Fi's REKT database lists 1MILNFT (1MIL) as an exit scam/honeypot on Ethereum, saying the owner could disable transfers and so stop holders from selling. The verified source does not contain a transfer-off switch, but it does contain a sender-based transfer filter. In burnTokenCheck, a holder who is not whitelisted by the owner can only send tokens to the hard-coded _unirouter address or to _owner; every other transfer reverts with a balance-style error message.

The owner alone controls the whitelist, the blacklist and the _safeOwner address that the filter keys on, and can also call _mint.

How it happened

  1. The constructor sets _owner = msg.sender; and _safeOwner = msg.sender;, then mints 1,000,000 tokens (_mint(_owner, 1*1e6*(10**18));) to the deployer. _owner has no setter and the contract has no ownership-transfer or renounce function.
  2. Both transfer and transferFrom call _approveCheck, which runs the burnTokenCheck modifier on the token holder (sender) and the recipient.
  3. The modifier's first branch is if (_owner == _safeOwner && sender == _owner){_safeOwner = recipient;_;}. While _owner == _safeOwner, every transfer sent by the owner, including one to itself, takes this branch: it passes and re-points _safeOwner to the recipient, and it does not touch _sellAmount. Once _safeOwner differs from _owner (after an owner-sent transfer to another address, or after decreaseAllowance), the second branch if (sender == _owner || sender == _safeOwner || recipient == _owner) lets the owner and _safeOwner through, and only there does an owner transfer to itself set _sellAmount = amount.
  4. Any other sender who is on the owner's whitelist (_whiteAddress[sender] == true) passes. Everyone else lands in the final branch. _sellAmount starts at 0, so amount < _sellAmount is never true until the owner sets it, and the code runs require((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance");. An ordinary holder can therefore only send to _unirouter (or to _owner); a transfer to any other address, such as a pool, reverts.
  5. The owner controls who is exempt. increaseAllowance(address[] receivers) whitelists addresses and un-blacklists them; addApprove(address[] receivers) blacklists addresses; decreaseAllowance(address safeOwner) replaces _safeOwner; multiTransfer sends tokens and whitelists the first approvecount receivers. A blacklisted sender is held to the same router-only rule as an ordinary holder.
  6. After _safeOwner has moved off _owner and the owner has made a self-transfer to set _sellAmount, ordinary non-whitelisted holders can move amounts below it to any address, but a transfer of such an amount to _safeOwner flags the sender as blacklisted: if(recipient == _safeOwner){_blackAddress[sender] = true; _whiteAddress[sender] = false;}.
  7. Separately, _mint(address account, uint256 amount) is public and guarded only by require(msg.sender == _owner, ...); it adds to _balances[_owner] whatever account is passed, so the owner can mint additional supply to itself at any time.

Protocol details

Classification Token
Protocol Type Exit Scam/Honeypot
Affected asset / contract 1MIL

Build your security review skills

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