Foxy.farm Rug Pull

Reported loss $6K
Ethereum
Rugpull

What happened

FOXY (foxy.farm) is an ERC-20 token whose mint(address _to, uint256 _amount) is restricted to the owner but has no supply cap, so the owner address can create any number of tokens at any time. Etherscan shows the deployer address 0x164ff155…1d2261 added 5 ETH and 1,000,000 FOXY of liquidity on Uniswap V2 on 2020-10-19 (tx 0xc9271907…77f2fd). On 2020-10-20 the same address called mint to create 1,000,000,000 FOXY for itself (tx 0x1780a928…3fd478) and about 95 seconds later swapped them on Uniswap V2 for roughly 21.86 WETH (tx 0x35cbcf78…69e71b).

The code also gives the owner a transfer gate it can tighten (checkValidAddress, checkValidContract) and an owner-only burn(address _from, uint256 _amount) that can destroy any holder's balance; none of the opened sources shows those two being used.

How it happened

  1. The deployer address 0x164ff155…1d2261 deployed the token on 2020-10-19 (tx 0x8ad85da8…913d89) and is its owner: contract FoxyFarm is ERC20("foxy.farm", "FOXY"), Ownable.
  2. Shortly afterwards it added 5 ETH and 1,000,000 FOXY to a new Uniswap V2 pair, so the pool's token side was only 1,000,000 FOXY.
  3. The owner-only mint has no cap and no timelock: function mint(address _to, uint256 _amount) public onlyOwner { holders[_to] = true; _mint(_to, _amount); }. The comment above it calls it a rewards mint. It also adds the recipient to the holders whitelist that the transfer gate checks.
  4. On 2020-10-20 at 17:48:29 UTC the deployer called mint for 1,000,000,000 FOXY to its own address, 1,000 times the amount it had put into the pool.
  5. At 17:50:04 UTC the deployer sold all of it with swapExactTokensForETH and received about 21.86 WETH. Selling a token amount that large into a pool holding 1,000,000 FOXY returns most of the pool's ETH side (constant-product arithmetic, not read from pool state).
  6. What the code additionally allows, not shown as used: function checkValidAddress(uint256 _addr) public onlyOwner sets contractLength, and isNotBot returns (size == 0) && contractLength == INT_RANGE, so once the owner sets contractLength to any value other than 65535, checkValidBalance (isNotBot(sender) || holders[sender] || holder < 1) fails for every non-whitelisted sender after the first transfer call. The owner whitelists addresses with checkValidContract, and function burn(address _from, uint256 _amount) public onlyOwner { _burn(_from, _amount); } needs no allowance.

Protocol details

Classification NFT
Protocol Type Exit Scam/Rugpull
Affected asset / contract FOXY

Build your security review skills

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