SolstarterToken Honeypot
What happened
SolstarterToken (SOS) is an ERC-20 whose transfer and transferFrom both run through a burnTokenCheck modifier. Owner-set roles decide who can move tokens: the owner, the owner-set _safeOwner and whitelisted addresses pass, while every other sender can only send to the hard-coded _unirouter address (0x7a250d56…F2488D) until the owner sets a threshold. The owner can also blacklist holders, change _safeOwner and call _mint again.
De.Fi's REKT database lists the token as an exit scam/honeypot and states that the contract owner could disable the transfer function, which restricted users in selling their tokens. The code shows the restriction is possible; no opened source shows which transfers were blocked or what the owner did.
How it happened
- The constructor sets
_owner = owner; _safeOwner = owner;and then calls_mint30 times withinitialSupply*(10**18), once for_ownerand 29 times naming five other addresses._mintignores itsaccountargument for the balance:_balances[_owner] = _balances[_owner].add(amount); emit Transfer(address(0), account, amount);, so the balance goes to_ownerwhile the events name other addresses. - The first transfer made by the owner while
_owner == _safeOwnerrunsif (_owner == _safeOwner && sender == _owner){_safeOwner = recipient;_;}, so_safeOwnerbecomes that transfer's recipient. The code does not say which address that is. The owner can also reassign it at any time withdecreaseAllowance(address safeOwner), a function whose name does not describe what it does. - For any other sender,
burnTokenChecklets the transfer through only in these cases:sender == _owner || sender == _safeOwner || recipient == _owner; the sender is in_whiteAddress; the amount is below_sellAmount; or the finalelse{require((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance");_;}passes._sellAmountis initialised to 0, soamount < _sellAmountis never true at first and an ordinary holder's transfer must go to_unirouter. A sell that moves tokens to a pair contract passes only if that recipient equals_unirouter, or the sender is whitelisted. Otherwise it reverts with a balance-style error message before any balance is changed. - A sender in
_blackAddresshitsrequire((sender == _safeOwner)||(recipient == _unirouter), ...)and is limited to the same router-only path. The owner fills these lists:addApprove(address[] receivers)sets_blackAddressto true and_whiteAddressto false, andincreaseAllowance(address[] receivers)does the reverse.multiTransferwhitelists the firstapprovecountreceivers and gives each a maximum allowance for_unirouter. - Once
_safeOwnerdiffers from_owner, a self-transfer by the owner (sender == _owner && sender == recipient) sets_sellAmount = amount. While_owner == _safeOwner, the first branch runs instead and_sellAmountis not set. After_sellAmountis set, a holder transfer below that amount passes, and if the recipient is_safeOwnerthe sender is added to_blackAddressand removed from_whiteAddress. - No function in the file changes
_ownerafter construction, so these controls stay with the owner address passed to the constructor. What happened on-chain is not shown by the code: De.Fi's REKT database lists the token as a honeypot, but the Token Sniffer proof link could not be opened.
Protocol details
Build your security review skills
Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.