Step.finance 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
- The constructor sets
_owner = owner;and_safeOwner = owner;, and_unirouteris hard-coded to 0x7a250d56…F2488D.transferandtransferFromboth call_approveCheck, which is guarded byburnTokenCheck._owneris assigned only in the constructor and there is no ownership-transfer or renounce function, so this control cannot be handed over or given up. - While
_owner == _safeOwner, the first transfer sent by the owner runsif (_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 withfunction decreaseAllowance(address safeOwner). - 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;}. - An ordinary holder, neither whitelisted nor blacklisted, reaches
if (amount < _sellAmount)._sellAmountstarts at 0, so the test is false andrequire((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance")runs. A sell sends tokens to the pool, not the router, so it reverts. - If the owner has set
_sellAmountabove zero, a transfer of strictly less than that amount passes, and whenrecipient == _safeOwnerit runs_blackAddress[sender] = true; _whiteAddress[sender] = false;. That sell succeeds once and the seller is blacklisted; an amount equal to or above_sellAmountreverts as in step 4. - 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, andmultiTransferwhitelists the firstapprovecountreceivers and approves the router for them. function _mint(address account, uint256 amount) publicrequiresmsg.sender == _ownerand 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
Build your security review skills
Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.