HoodToken

0x364187464b4e74256229054a71f23c00c308c0de

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
6,721 bytes
ABI entries
56
24 read · 11 write
License
none

Contract information

Address
0x364187464b4e74256229054a71f23c00c308c0de
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x4f89efdc25…a96506d030

Token

Name
Hood Dog
Symbol
HOODDOG
Decimals
18
Holders
5

Read contract (24)

allowance(address, address)uint256
balanceOf(address)uint256
contractURI()string
creator()address
decimals()uint8
description()string
feeLocker()address
image()string
isExempt(address)bool
isFeeVenue(address)bool
launchBlock()uint256
launcher()address
logo()string
maxWalletAmount()uint256
name()string
ownerRegistry()address
pool()address
positionManager()address
restrictionEndBlock()uint256
socials()string
swapRouter()address
symbol()string
tokenOwner()address
totalSupply()uint256

Events (8)

ApprovalContractURIUpdatedDescriptionUpdatedFeeVenueAddedImageUpdatedPoolSetSocialsUpdatedTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name_",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol_",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "supply_",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "creator_",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "string",
            "name": "image",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "description",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "socials",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "contractURI",
            "type": "string"
          }
        ],
        "internalType": "struct HoodToken.Metadata",
        "name": "metadata_",
        "type": "tuple"
      },
      {
        "internalType": "uint16",
        "name": "maxWalletBps_",
        "type": "uint16"
      },
      {
        "internalType": "uint32",
        "name": "restrictionBlocks_",
        "type": "uint32"
      },
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeLocker_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "swapRouter_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "ownerRegistry_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "allowance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "ERC20InsufficientAllowance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "ERC20InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approver",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidApprover",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidSender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidSpender",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LaunchBlockBuy",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxWalletExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyLauncher",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyTokenOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "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": [],
    "name": "ContractURIUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "description",
        "type": "string"
      }
    ],
    "name": "DescriptionUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "feeVenue",
        "type": "address"
      }
    ],
    "name": "FeeVenueAdded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "image",
        "type": "string"
      }
    ],
    "name": "ImageUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "name": "PoolSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "socials",
        "type": "string"
      }
    ],
    "name": "SocialsUpdated",
    "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": [
      {
        "internalType": "address",
        "name": "venue",
        "type": "address"
      }
    ],
    "name": "addFeeVenue",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "allowance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "burn",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "burnFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "contractURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creator",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "decimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "description",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeLocker",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "image",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "isExempt",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "venue",
        "type": "address"
      }
    ],
    "name": "isFeeVenue",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchBlock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launcher",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "logo",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxWalletAmount",
    "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": "ownerRegistry",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pool",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "restrictionEndBlock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "uri",
        "type": "string"
      }
    ],
    "name": "setContractURI",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "description_",
        "type": "string"
      }
    ],
    "name": "setDescription",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "image_",
        "type": "string"
      }
    ],
    "name": "setImage",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "pool_",
        "type": "address"
      }
    ],
    "name": "setPool",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "socials_",
        "type": "string"
      }
    ],
    "name": "setSocials",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "socials",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "swapRouter",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "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": "value",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ITokenOwnerRegistry} from "./interfaces/ITokenOwnerRegistry.sol";

/**
 * @title HoodToken
 * @notice The ERC-20 deployed for every hood.dev launch. Fixed supply, minted
 *         in full to the launcher (which immediately LPs it single-sided into
 *         Uniswap V3 and locks the position in the FeeLocker). No owner, no
 *         mint, no pause — the only post-launch mutability is creator-editable
 *         metadata and the launcher's one-time `setPool` inside the launch tx.
 *
 *         Metadata is belt-and-suspenders so every indexer finds something it
 *         understands: on-chain getters (`image`/`logo`/`description`/
 *         `socials`) à la NOXA/Clanker, plus ERC-7572 `contractURI()`, plus
 *         the launcher's rich TokenLaunched event.
 *
 *         Optional launch protection (chosen by the creator at launch,
 *         constants immutable afterwards, auto-expiring):
 *         - buys straight from the pool are blocked in the launch block
 *           (except the creator's own atomic dev buy);
 *         - wallets are capped at `maxWalletAmount` until
 *           `restrictionEndBlock`.
 */
contract HoodToken is ERC20, ERC20Burnable {
    // ------------------------------------------------------------- immutables

    address public immutable launcher; // the factory; deployed this token
    /// @notice The ORIGINAL launch creator (historical identity; launch-block
    ///         dev-buy exemption). Control is NOT keyed on this — see
    ///         `tokenOwner()` / the TokenOwnerRegistry.
    address public immutable creator;
    /// @notice Canonical ownership registry; zero => `creator` is the owner
    ///         forever (standalone deployments only — the launcher always
    ///         wires a registry).
    address public immutable ownerRegistry;
    address public immutable positionManager;
    address public immutable feeLocker;
    address public immutable swapRouter;
    uint256 public immutable launchBlock;
    /// @notice Last block the transfer restrictions apply in. == launchBlock when disabled.
    uint256 public immutable restrictionEndBlock;
    /// @notice Max balance per non-exempt wallet during the restriction window. 0 = no cap.
    uint256 public immutable maxWalletAmount;

    // ---------------------------------------------------------------- storage

    /// @notice The token's Uniswap V3 pool. Set once by the launcher inside the
    ///         launch tx, before any third party can interact — de-facto immutable.
    address public pool;

    /// @notice Fee-policy venues (staking pool, vesting vault, burn/buyback
    ///         modules) registered by the launcher inside the launch tx —
    ///         exempt from the wallet cap so fee routing can never be bricked.
    mapping(address venue => bool) public isFeeVenue;

    string private _image;
    string private _description;
    string private _socials; // single JSON string, e.g. {"x":"...","telegram":"...","website":"..."}
    string private _contractURI; // ERC-7572: ipfs:// JSON with the full metadata

    // ----------------------------------------------------------------- events

    /// @dev ERC-7572: carries no args; indexers re-read contractURI().
    event ContractURIUpdated();
    event ImageUpdated(string image);
    event DescriptionUpdated(string description);
    event SocialsUpdated(string socials);
    event PoolSet(address indexed pool);
    event FeeVenueAdded(address indexed feeVenue);

    // ----------------------------------------------------------------- errors

    error OnlyLauncher();
    error OnlyTokenOwner();
    error PoolAlreadySet();
    error ZeroAddress();
    error LaunchBlockBuy();
    error MaxWalletExceeded();

    // ------------------------------------------------------------ constructor

    struct Metadata {
        string image;
        string description;
        string socials;
        string contractURI;
    }

    constructor(
        string memory name_,
        string memory symbol_,
        uint256 supply_,
        address creator_,
        Metadata memory metadata_,
        uint16 maxWalletBps_, // 0 disables the wallet cap
        uint32 restrictionBlocks_, // 0 limits restrictions to the launch block itself
        address positionManager_,
        address feeLocker_,
        address swapRouter_,
        address ownerRegistry_ // 0 => creator owns forever (standalone use)
    ) ERC20(name_, symbol_) {
        launcher = msg.sender;
        creator = creator_;
        positionManager = positionManager_;
        feeLocker = feeLocker_;
        swapRouter = swapRouter_;
        ownerRegistry = ownerRegistry_;

        launchBlock = block.number;
        restrictionEndBlock = block.number + restrictionBlocks_;
        maxWalletAmount = maxWalletBps_ == 0 ? 0 : (supply_ * maxWalletBps_) / 10_000;

        _image = metadata_.image;
        _description = metadata_.description;
        _socials = metadata_.socials;
        _contractURI = metadata_.contractURI;

        _mint(msg.sender, supply_);
    }

    // --------------------------------------------------------------- metadata

    /// @notice ERC-7572 contract-level metadata URI (ipfs:// JSON).
    function contractURI() external view returns (string memory) {
        return _contractURI;
    }

    /// @notice Token artwork URI. `logo()` is an alias for NOXA-style readers.
    function image() external view returns (string memory) {
        return _image;
    }

    function logo() external view returns (string memory) {
        return _image;
    }

    function description() external view returns (string memory) {
        return _description;
    }

    /// @notice Social links as a single JSON string.
    function socials() external view returns (string memory) {
        return _socials;
    }

    function setContractURI(string calldata uri) external onlyTokenOwner {
        _contractURI = uri;
        emit ContractURIUpdated();
    }

    function setImage(string calldata image_) external onlyTokenOwner {
        _image = image_;
        emit ImageUpdated(image_);
    }

    function setDescription(string calldata description_) external onlyTokenOwner {
        _description = description_;
        emit DescriptionUpdated(description_);
    }

    function setSocials(string calldata socials_) external onlyTokenOwner {
        _socials = socials_;
        emit SocialsUpdated(socials_);
    }

    /// @notice Canonical owner of this token (metadata + fee-recipient
    ///         control). Resolves through the TokenOwnerRegistry; zero after
    ///         a renounce (metadata frozen).
    function tokenOwner() public view returns (address) {
        return ownerRegistry == address(0) ? creator : ITokenOwnerRegistry(ownerRegistry).ownerOf(address(this));
    }

    // ----------------------------------------------------------------- launch

    /// @notice One-time pool registration by the launcher, called inside launch().
    function setPool(address pool_) external {
        if (msg.sender != launcher) revert OnlyLauncher();
        if (pool != address(0)) revert PoolAlreadySet();
        if (pool_ == address(0)) revert ZeroAddress();
        pool = pool_;
        emit PoolSet(pool_);
    }

    /// @notice Fee-venue registration by the launcher inside launch(), once
    ///         per chosen fee policy (token-side and/or creator-side).
    ///         Idempotent: both policies may share one venue (staking pool).
    function addFeeVenue(address venue) external {
        if (msg.sender != launcher) revert OnlyLauncher();
        if (venue == address(0)) revert ZeroAddress();
        if (isFeeVenue[venue]) return;
        isFeeVenue[venue] = true;
        emit FeeVenueAdded(venue);
    }

    // ----------------------------------------------------------- restrictions

    /// @notice Infrastructure addresses the restrictions never apply to.
    ///         The pool must be exempt as a recipient or sells would trip the
    ///         wallet cap; the FeeLocker must be exempt so fee collection can
    ///         never be bricked by the cap; the fee venue (staking pool /
    ///         vesting vault) must be exempt so fee routing and staking
    ///         deposits work during the restriction window.
    function isExempt(address account) public view returns (bool) {
        return account == launcher || account == pool || account == positionManager || account == feeLocker
            || account == swapRouter || isFeeVenue[account];
    }

    function _update(address from, address to, uint256 value) internal override {
        // Mint/burn always passes; after the restriction window this is two
        // immutable reads and nothing else.
        if (from != address(0) && to != address(0) && block.number <= restrictionEndBlock && !isExempt(to)) {
            // Launch-block buys are blocked for everyone except the creator's
            // own atomic dev buy (pool -> creator, still wallet-capped below).
            if (block.number == launchBlock && from == pool && to != creator) revert LaunchBlockBuy();
            if (maxWalletAmount != 0 && balanceOf(to) + value > maxWalletAmount) revert MaxWalletExceeded();
        }
        super._update(from, to, value);
    }
    // ---------------------------------------------------------------- helpers

    modifier onlyTokenOwner() {
        if (msg.sender != tokenOwner()) revert OnlyTokenOwner();
        _;
    }
}
Chain explorer3495msChain node110ms