Palm.io Honeypot
What happened
De.Fi's REKT database lists Palm.io (PALM) on Ethereum as an Exit Scam/Honeypot, noting that the contract owner could disable the transfer function and so stop users selling. The verified source at 0x0516cbb0…0fdc8b does not switch every transfer off. Each transfer and transferFrom runs through a burnTokenCheck modifier under which, by default, a holder's tokens can only be sent to one hard-coded address, _unirouter; a transfer to any other destination reverts with ERC20: transfer amount exceeds balance even when the balance is sufficient.
The owner can whitelist or blacklist addresses and repoint the privileged _safeOwner. The constructor also calls _mint ten times, and because _mint always credits _owner, all ten amounts land in the owner's balance while nine of the Transfer events name other addresses. The code shows how sales can be blocked; no transaction showing a blocked sale was opened for this record.
How it happened
- The constructor sets
_owner = owner;and_safeOwner = owner;, then calls_mintten times: once with_mint(_owner, initialSupply*(10**18));and nine more with the addresses 0xF326e4dE…9f1915 (five calls), 0x41653c7d…142Cfb (two calls) and 0xA8C6F6e2…d83985 (two calls). _mintignores itsaccountargument for the balance:_balances[_owner] = _balances[_owner].add(amount);credits the owner, whileemit Transfer(address(0), account, amount);namesaccount. All ten mints therefore end up in the owner's balance, and the three other addresses appear only in event logs.transferandtransferFromboth call_approveCheck(...), declaredinternal burnTokenCheck(sender,recipient,amount), so the modifier runs on every holder-to-holder move.- The owner is exempt. While
_owner == _safeOwner, the first transfer sent by the owner runs_safeOwner = recipient;and passes. After that, transfers pass wheneversender == _owner || sender == _safeOwner || recipient == _owner. - An ordinary holder (not whitelisted, not blacklisted) falls to the last branch.
_sellAmountstarts at 0, soamount < _sellAmountis false and the code reachesrequire((sender == _safeOwner)||(recipient == _unirouter), "ERC20: transfer amount exceeds balance");. Since the holder is not_safeOwner, the transfer only succeeds if the recipient is_unirouter; any other recipient reverts. The same check applies to whichever address is the sender, including a pool contract paying out tokens, unless that address is whitelisted or is_safeOwner. - The owner can change who is exempt.
increaseAllowance(address[] receivers)sets_whiteAddress[...] = true, which skips all checks for those senders.addApprove(address[] receivers)sets_blackAddress[...] = true, limiting those senders torecipient == _unirouter.decreaseAllowance(address safeOwner)sets_safeOwner.multiTransferwhitelists the firstapprovecountreceivers and sets a maximum allowance for_unirouteron their behalf. - If the owner sends tokens to itself (once
_safeOwneris no longer the owner),_sellAmount = amount;is set. A non-privileged transfer smaller than_sellAmountthen passes, and if its recipient is_safeOwnerthe sender is blacklisted by_blackAddress[sender] = true;. - No function changes or renounces
_owner, and_mintstayspublicbehindrequire(msg.sender == _owner, ...), so the owner can add to its own balance later. Whether any of these powers were used is not shown by the code.
Protocol details
Build your security review skills
Work through hands-on labs covering real exploit mechanics, review techniques, and defensive patterns.