Step.finance Honeypot

Reported loss Not disclosed
Ethereum
Honeypot

What happened

STEP (on-chain name Step.finance, Ethereum contract 0x327d5e8f…99a594, 21,000,000 tokens at 18 decimals) runs every transfer and transferFrom through a modifier, burnTokenCheck. It lets the owner, an owner-chosen _safeOwner and whitelisted addresses move tokens freely, but an ordinary holder can only send tokens to the owner or to the hard-coded _unirouter address 0x7a250d56…F2488D. A sell into a trading pool sends tokens to the pool, so it reverts with the misleading message "ERC20: transfer amount exceeds balance", unless the amount is below an owner-set _sellAmount, in which case it succeeds once and flags the seller.

De.Fi's REKT database lists the token as an Exit Scam/Honeypot, stating that the contract owner could disable the transfer function, which restricted users in selling their tokens; no pool address, sell transaction or loss figure was opened to confirm what happened on-chain.

How it happened

  1. The constructor sets _owner = owner; and _safeOwner = owner;, and _unirouter is hard-coded to 0x7a250d56…F2488D. transfer and transferFrom both call _approveCheck, which is guarded by burnTokenCheck. _owner is assigned only in the constructor and there is no ownership-transfer or renounce function, so this control cannot be handed over or given up.
  2. While _owner == _safeOwner, the first transfer sent by the owner runs if (_owner == _safeOwner && sender == _owner){_safeOwner = recipient;_;}, so its recipient becomes _safeOwner. If that first transfer is the liquidity deposit, the pool becomes _safeOwner. The owner can also reassign it with function decreaseAllowance(address safeOwner).
  3. Privileged paths pass unconditionally: if (sender == _owner || sender == _safeOwner || recipient == _owner). If the pool is _safeOwner, buys (sender is the pool) pass, and anyone can send tokens to the owner. The owner can set the sell threshold by sending tokens to itself: if (sender == _owner && sender == recipient){_sellAmount = amount;}.
  4. An ordinary holder, neither whitelisted nor blacklisted, reaches if (amount < _sellAmount). _sellAmount starts at 0, so the test is false and require((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance") runs. A sell sends tokens to the pool, not the router, so it reverts.
  5. If the owner has set _sellAmount above zero, a transfer of strictly less than that amount passes, and when recipient == _safeOwner it runs _blackAddress[sender] = true; _whiteAddress[sender] = false;. That sell succeeds once and the seller is blacklisted; an amount equal to or above _sellAmount reverts as in step 4.
  6. A blacklisted sender hits require((sender == _safeOwner)||(recipient == _unirouter), ...) and can only send tokens to the router address, so further sells to the pool revert. A whitelisted sender (_whiteAddress[sender] == true) passes freely. The owner controls both lists through functions with unrelated names: increaseAllowance(address[]) whitelists, addApprove(address[]) blacklists, and multiTransfer whitelists the first approvecount receivers and approves the router for them.
  7. function _mint(address account, uint256 amount) public requires msg.sender == _owner and adds the amount to _balances[_owner], so the owner can create more tokens. The source does not show which of these functions were called or when.

Protocol details

Classification Token
Protocol Type Portfolio Tracker
Affected asset / contract STEP
Protocol links Website @StepFinance_

Build your security review skills

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