Palm.io Honeypot

Reported loss Not disclosed
Ethereum
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

  1. The constructor sets _owner = owner; and _safeOwner = owner;, then calls _mint ten 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).
  2. _mint ignores its account argument for the balance: _balances[_owner] = _balances[_owner].add(amount); credits the owner, while emit Transfer(address(0), account, amount); names account. All ten mints therefore end up in the owner's balance, and the three other addresses appear only in event logs.
  3. transfer and transferFrom both call _approveCheck(...), declared internal burnTokenCheck(sender,recipient,amount), so the modifier runs on every holder-to-holder move.
  4. The owner is exempt. While _owner == _safeOwner, the first transfer sent by the owner runs _safeOwner = recipient; and passes. After that, transfers pass whenever sender == _owner || sender == _safeOwner || recipient == _owner.
  5. An ordinary holder (not whitelisted, not blacklisted) falls to the last branch. _sellAmount starts at 0, so amount < _sellAmount is false and the code reaches require((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.
  6. 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 to recipient == _unirouter. decreaseAllowance(address safeOwner) sets _safeOwner. multiTransfer whitelists the first approvecount receivers and sets a maximum allowance for _unirouter on their behalf.
  7. If the owner sends tokens to itself (once _safeOwner is no longer the owner), _sellAmount = amount; is set. A non-privileged transfer smaller than _sellAmount then passes, and if its recipient is _safeOwner the sender is blacklisted by _blackAddress[sender] = true;.
  8. No function changes or renounces _owner, and _mint stays public behind require(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

Classification Token
Protocol Type Exit Scam/Honeypot
Affected asset / contract PALM

Build your security review skills

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