CultOfRobinhood

0xaf6a7b30f68846ed5ea7110ba583dcfdad37f9e2

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
5,308 bytes
ABI entries
35
19 read · 8 write
License
none

Contract information

Address
0xaf6a7b30f68846ed5ea7110ba583dcfdad37f9e2
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x8975473a9e…b2ca13aff3

Token

Name
CultOfRobinhood
Symbol
MERRYMEN
Decimals
18
Holders
995

Read contract (19)

AIRDROP_SUPPLY()uint256
INITIAL_MAX_WALLET_BPS()uint256
POSITION_MANAGER()address
TOTAL_SUPPLY()uint256
allowance(address, address)uint256
balanceOf(address)uint256
decimals()uint8
isExempt(address)bool
lpNFTId()uint256
maxWallet()uint256
name()string
owner()address
positionManager()address
spawner()address
symbol()string
totalSupply()uint256
tradingEnabled()bool
uniswapV3Pool()address
walletLimited()bool

Events (7)

AirdropApprovalExemptSetMaxWalletSetOwnershipTransferredStartedTradingTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_owner",
        "type": "address"
      },
      {
        "internalType": "address[]",
        "name": "recipients",
        "type": "address[]"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "recipients",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountEach",
        "type": "uint256"
      }
    ],
    "name": "Airdrop",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "tokenOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "exempt",
        "type": "bool"
      }
    ],
    "name": "ExemptSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "bps",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "maxWallet",
        "type": "uint256"
      }
    ],
    "name": "MaxWalletSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "lpNFTId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "positionManager",
        "type": "address"
      }
    ],
    "name": "StartedTrading",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "AIRDROP_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "INITIAL_MAX_WALLET_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POSITION_MANAGER",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "o",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "s",
        "type": "address"
      }
    ],
    "name": "allowance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "a",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "decimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "bps",
        "type": "uint256"
      }
    ],
    "name": "increaseWalletLimits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isExempt",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lpNFTId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxWallet",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "recoverERC20",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "exempt",
        "type": "bool"
      }
    ],
    "name": "setExempt",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "spawner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "pmgr",
        "type": "address"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      },
      {
        "components": [
          {
            "internalType": "address",
            "name": "token0",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "token1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "uint256",
            "name": "amount0Desired",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "amount1Desired",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "amount0Min",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "amount1Min",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "recipient",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "deadline",
            "type": "uint256"
          }
        ],
        "internalType": "struct INonfungiblePositionManager.MintParams",
        "name": "params",
        "type": "tuple"
      }
    ],
    "name": "startTrading",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tradingEnabled",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "uniswapV3Pool",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "walletLimited",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
//
// =====================================================================
//   ____      _ _      ___   __   ___      _     _       _                 _
//  / ___|   _| | |_   / _ \ / _| / _ \ ___| |__ (_)_ __ | |__   ___   ___ | |
// | |  | | | | | __| | | | | |_ | | | / _ \ '_ \| | '_ \| '_ \ / _ \ / _ \| |
// | |__| |_| | | |_  | |_| |  _|| |_| | (_) | |_) | | | | | | | | (_) | (_) |_|
//  \____\__,_|_|\__|  \___/|_|(_)\___/ \___/|_.__/|_|_| |_|_| |_|\___/ \___/(_)
// =====================================================================
//
// CultOfRobinhood ($MERRYMEN) — cultofrobinhood.space
//
pragma solidity ^0.8.25;

import {INonfungiblePositionManager} from "./RobinhoodInterfaces.sol";

/**
 * @title CultOfRobinhood ($MERRYMEN)
 * @notice Fixed-supply ERC-20 with a genesis airdrop performed atomically in the
 *         constructor: `AIRDROP_SUPPLY` is split EQUALLY across the snapshot of
 *         top-1000 CASHCAT holders (see scripts/fetch-holders.js), and the
 *         remainder is minted to `owner` to seed liquidity / treasury.
 *
 *         There is no mint after genesis — `totalSupply()` is permanently
 *         `TOTAL_SUPPLY`. The owner has NO power over balances; it can only
 *         `recoverERC20` (rescue unrelated tokens sent here by mistake) and manage
 *         the anti-whale cap (`increaseWalletLimits`, `setExempt`). Renounceable.
 *
 *         Launches with a 1.5% max-wallet cap (INITIAL_MAX_WALLET_BPS); non-exempt
 *         addresses cannot receive tokens beyond that. The owner/deployer/contract
 *         are exempt so genesis + LP seeding work; the cap can only be loosened.
 *
 *         Distribution (TOTAL_SUPPLY = 1,000,000,000):
 *           - AIRDROP_SUPPLY = 500,000,000  → split equally among `recipients`
 *           - remainder      = 500,000,000  → held by THIS contract, seeded
 *             one-sided into a Uniswap V3 LP by `startTrading` (LP NFT → owner).
 */
contract CultOfRobinhood {
    // ---- ERC-20 metadata ----
    string public constant name = "CultOfRobinhood";
    string public constant symbol = "MERRYMEN";
    uint8 public constant decimals = 18;

    // ---- supply constants ----
    uint256 public constant TOTAL_SUPPLY = 1_000_000_000 * 1e18; // 1B
    uint256 public constant AIRDROP_SUPPLY = 500_000_000 * 1e18;  // 50% to holders

    // ---- max-wallet (anti-whale) ----
    // Limit is expressed in basis points of TOTAL_SUPPLY (10000 = 100% = unlimited).
    uint256 public constant INITIAL_MAX_WALLET_BPS = 150; // start at 1.5%
    uint256 public maxWallet;      // max tokens a non-exempt address may hold
    bool public walletLimited;     // false once the limit is lifted to 100%
    mapping(address => bool) public isExempt; // addresses immune to the limit

    // Robinhood Chain (Uniswap V3) NonfungiblePositionManager. It routes MERRYMEN
    // when the reserve is seeded into the LP, so it is exempted from the cap in the
    // constructor (startTrading also exempts whatever pmgr it is given, plus the
    // pool inline). The swap router needs NO exemption: V3 swaps settle directly
    // between the pool and the user via the swap callback, so the router never
    // custodies MERRYMEN.
    address public constant POSITION_MANAGER = 0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3;

    // ---- liquidity (one-sided Uniswap V3 seed) ----
    bool public tradingEnabled;                 // set once by startTrading
    INonfungiblePositionManager public positionManager;
    address public uniswapV3Pool;               // exempted inline at creation
    uint256 public lpNFTId;                     // LP position NFT (owned by `owner`)

    // ---- state ----
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;
    uint256 private _totalSupply;
    address public owner;
    address public spawner; // deployer (may be the CultSpawn contract); can startTrading

    // ---- events ----
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed tokenOwner, address indexed spender, uint256 value);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    event Airdrop(uint256 recipients, uint256 amountEach);
    event MaxWalletSet(uint256 bps, uint256 maxWallet);
    event ExemptSet(address indexed account, bool exempt);
    event StartedTrading(address indexed pool, uint256 lpNFTId, address positionManager);

    /**
     * @param _owner      receives the non-airdropped remainder (LP / treasury).
     * @param recipients  the eligible top-1000 CASHCAT holders (must be unique,
     *                     non-zero). Each receives `AIRDROP_SUPPLY / len` tokens.
     */
    constructor(address _owner, address[] memory recipients) {
        require(_owner != address(0), "owner=0");
        uint256 n = recipients.length;
        require(n > 0, "no recipients");
        require(AIRDROP_SUPPLY >= n, "airdrop too small");

        owner = _owner;
        spawner = msg.sender;
        emit OwnershipTransferred(address(0), _owner);

        // Anti-whale cap: start at 1.5% of supply. Exempt the parties that must
        // legitimately hold or route more than the cap — the owner (holds the 50%
        // reserve for LP), the deployer, and this contract. The owner can exempt
        // the LP pool / router before seeding liquidity via setExempt().
        maxWallet = (TOTAL_SUPPLY * INITIAL_MAX_WALLET_BPS) / 10000;
        walletLimited = true;
        _setExempt(_owner, true);
        _setExempt(msg.sender, true); // deployer / spawner
        _setExempt(address(this), true);
        _setExempt(POSITION_MANAGER, true); // exempted up front so LP seeding never hits the cap
        emit MaxWalletSet(INITIAL_MAX_WALLET_BPS, maxWallet);

        // Equal split; integer division leaves dust that rolls into the remainder.
        uint256 amountEach = AIRDROP_SUPPLY / n;
        uint256 dropped;
        for (uint256 i = 0; i < n; i++) {
            address to = recipients[i];
            require(to != address(0), "recipient=0");
            // Overwrite (not +=): the snapshot is deduped, so a first-write model
            // makes an accidental duplicate a no-op rather than a double-mint.
            require(_balances[to] == 0, "duplicate");
            _balances[to] = amountEach;
            emit Transfer(address(0), to, amountEach);
            unchecked { dropped += amountEach; }
        }

        // The undropped remainder (the 500M LP reserve + rounding dust) is held by
        // THIS contract, to be seeded one-sided into the Uniswap V3 LP by
        // startTrading(). address(this) is exempt, so it is unaffected by the cap.
        uint256 remainder = TOTAL_SUPPLY - dropped;
        _balances[address(this)] += remainder;
        emit Transfer(address(0), address(this), remainder);

        _totalSupply = TOTAL_SUPPLY;
        emit Airdrop(n, amountEach);
    }

    // ---- ERC-20 views ----
    function totalSupply() external view returns (uint256) { return _totalSupply; }
    function balanceOf(address a) external view returns (uint256) { return _balances[a]; }
    function allowance(address o, address s) external view returns (uint256) { return _allowances[o][s]; }

    // ---- ERC-20 core ----
    function transfer(address to, uint256 amount) external returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        _approve(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        uint256 a = _allowances[from][msg.sender];
        if (a != type(uint256).max) {
            require(a >= amount, "allowance");
            unchecked { _approve(from, msg.sender, a - amount); }
        }
        _transfer(from, to, amount);
        return true;
    }

    function _transfer(address from, address to, uint256 amount) private {
        require(from != address(0) && to != address(0), "zero");
        uint256 bal = _balances[from];
        require(bal >= amount, "balance");
        // Anti-whale: a non-exempt recipient may not end up holding > maxWallet.
        if (walletLimited && !isExempt[to]) {
            require(_balances[to] + amount <= maxWallet, "max wallet");
        }
        unchecked {
            _balances[from] = bal - amount;
            _balances[to] += amount;
        }
        emit Transfer(from, to, amount);
    }

    function _approve(address o, address s, uint256 amount) private {
        require(o != address(0) && s != address(0), "zero");
        _allowances[o][s] = amount;
        emit Approval(o, s, amount);
    }

    // ---- liquidity: one-sided Uniswap V3 seed (owner) ----

    /// Create the MERRYMEN/WETH pool and seed the entire contract-held reserve
    /// one-sided into it; the LP position NFT is minted to `owner`. Callable once.
    /// @dev Mirrors LootProtocolToken.startTrading / LootSpawn.start. The caller
    ///      (an off-chain script) supplies the sorted token0/token1, the initial
    ///      `sqrtPriceX96`, and a tick range placed entirely on the MERRYMEN side
    ///      of the price so no WETH is required. The position manager and the pool
    ///      are exempt (this contract already is), so the seed transfer bypasses
    ///      the max-wallet cap.
    function startTrading(
        address pmgr,
        uint160 sqrtPriceX96,
        INonfungiblePositionManager.MintParams calldata params
    ) external {
        require(msg.sender == owner || msg.sender == spawner, "not owner");
        require(!tradingEnabled, "trading on");
        require(params.recipient == owner, "recipient");
        require(params.token0 != params.token1, "same token");
        require(params.token0 == address(this) || params.token1 == address(this), "not this token");

        // Seed exactly the reserve this contract holds (one-sided: only MERRYMEN).
        uint256 lpAmount = _balances[address(this)];
        require(lpAmount > 0, "no reserve");
        if (params.token1 == address(this)) {
            require(params.amount1Desired == lpAmount && params.amount0Desired == 0, "amounts");
        } else {
            require(params.amount0Desired == lpAmount && params.amount1Desired == 0, "amounts");
        }

        tradingEnabled = true;
        positionManager = INonfungiblePositionManager(pmgr);
        _setExempt(pmgr, true);                       // no-op if pmgr is the constant
        _allowances[address(this)][pmgr] = lpAmount;  // let the PM pull the reserve

        address pool = positionManager.createAndInitializePoolIfNecessary(
            params.token0, params.token1, params.fee, sqrtPriceX96
        );
        uniswapV3Pool = pool;
        _setExempt(pool, true);                       // exempt the pool inline

        (lpNFTId, , , ) = positionManager.mint(params);
        emit StartedTrading(pool, lpNFTId, pmgr);
    }

    // ---- max-wallet controls (owner) ----

    /// Raise the max-wallet cap. Monotonic: the new cap must be >= the current
    /// one (owner can loosen, never tighten), and is expressed in basis points of
    /// TOTAL_SUPPLY. Passing 10000 (100%) lifts the limit permanently.
    function increaseWalletLimits(uint256 bps) external {
        require(msg.sender == owner, "not owner");
        require(walletLimited, "unlimited");
        require(bps <= 10000, "bps>100%");
        uint256 newMax = (TOTAL_SUPPLY * bps) / 10000;
        require(newMax >= maxWallet, "can only raise");
        maxWallet = newMax;
        if (bps == 10000) walletLimited = false; // limit lifted for good
        emit MaxWalletSet(bps, newMax);
    }

    /// Whitelist/mark an address immune to the max-wallet cap (e.g. the LP pool
    /// after creation, extra routers, CEX deposit wallets). Owner only.
    function setExempt(address account, bool exempt) external {
        require(msg.sender == owner, "not owner");
        _setExempt(account, exempt);
    }

    function _setExempt(address account, bool exempt) private {
        require(account != address(0), "zero");
        isExempt[account] = exempt;
        emit ExemptSet(account, exempt);
    }

    // ---- ownership (recovery + limit control; no power over balances) ----
    function transferOwnership(address newOwner) external {
        require(msg.sender == owner, "not owner");
        emit OwnershipTransferred(owner, newOwner); // newOwner=0 renounces
        owner = newOwner;
    }

    /// Rescue unrelated ERC-20s sent to this contract by mistake. Cannot touch
    /// $MERRYMEN balances of any holder — it only moves this contract's own
    /// balance of some *other* token, which is normally zero.
    function recoverERC20(address token, address to, uint256 amount) external {
        require(msg.sender == owner, "not owner");
        require(token != address(this), "self");
        (bool ok, bytes memory data) = token.call(
            abi.encodeWithSelector(0xa9059cbb, to, amount) // transfer(address,uint256)
        );
        require(ok && (data.length == 0 || abi.decode(data, (bool))), "recover failed");
    }
}
Chain explorer3515msChain node90ms