FlowTokenV3

0xbd18ca11450b75a35eed877420b461d083f1737d

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
9,729 bytes
ABI entries
74
34 read · 19 write
License
none

Contract information

Address
0xbd18ca11450b75a35eed877420b461d083f1737d
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x4d2eae5119…35742da6de

Token

Name
KIT
Symbol
KIT
Decimals
18
Holders
2

Read contract (34)

BPS()uint256
allowance(address, address)uint256
balanceOf(address)uint256
basket(address)uint256
basketOf(address)uint256
burned()uint256
curve()address
decimals()uint8
graduated()bool
holdDuration(address)uint256
holdSince(address)uint256
hook()address
initialSupply()uint256
isAMMPair(address)bool
isExcludedFromFees(address)bool
isExcludedFromLimits(address)bool
isExcludedFromRewards(address)bool
magnifiedRewardPerShare()uint256
maxTx()uint256
maxWallet()uint256
name()string
owner()address
pendingClaim(address)uint256
poolManager()address
positionManager()address
restricted()bool
rewardCheckpoint(address)uint256
symbol()string
totalShares()uint256
totalSupply()uint256
tradingEnabled()bool
treasury()address
universalRouter()address
usdg()address

Events (10)

ApprovalAutoPaidOnSellBasketFilledBasketMovedClaimedHookSetOwnershipTransferredPendingCreditedTradingEnabledTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name_",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol_",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "supply_",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "usdg_",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "restricted_",
        "type": "bool"
      },
      {
        "internalType": "address",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "universalRouter_",
        "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": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "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": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "who",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "usdg",
        "type": "uint256"
      }
    ],
    "name": "AutoPaidOnSell",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "usdgAdded",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newPerShare",
        "type": "uint256"
      }
    ],
    "name": "BasketFilled",
    "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": "amount",
        "type": "uint256"
      }
    ],
    "name": "BasketMoved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "who",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "usdg",
        "type": "uint256"
      }
    ],
    "name": "Claimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "hook",
        "type": "address"
      }
    ],
    "name": "HookSet",
    "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": "who",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "usdg",
        "type": "uint256"
      }
    ],
    "name": "PendingCredited",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "TradingEnabled",
    "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": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "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": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "basket",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "basketOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "burned",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claim",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "curve",
    "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": "enableTrading",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "fundBaskets",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "graduated",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "holdDuration",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "holdSince",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "initialSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isAMMPair",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isExcludedFromFees",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isExcludedFromLimits",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isExcludedFromRewards",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "magnifiedRewardPerShare",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxTx",
    "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": "onGraduate",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "pendingClaim",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "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": "removeLimits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "restricted",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "rewardCheckpoint",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "p",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "v",
        "type": "bool"
      }
    ],
    "name": "setAMMPair",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "c",
        "type": "address"
      }
    ],
    "name": "setCurve",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "a",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "v",
        "type": "bool"
      }
    ],
    "name": "setExcludedFromFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "a",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "v",
        "type": "bool"
      }
    ],
    "name": "setExcludedFromLimits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "a",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "v",
        "type": "bool"
      }
    ],
    "name": "setExcludedFromRewards",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "h",
        "type": "address"
      }
    ],
    "name": "setHook",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "maxTx_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxWallet_",
        "type": "uint256"
      }
    ],
    "name": "setLimits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "t",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalShares",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "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": "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"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "universalRouter",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "usdg",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "withdrawPending",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

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

/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    FlowTokenV3 — v3 reward token (contract #1)  ⚠️ WIP, NOT AUDITED

    The plain reward token for the v3 launchpad. Rewards accrue in USDG (the chain's USD base), streamed
    pro-rata by balance via the O(1) MasterChef accumulator (same as FLOWv4). Differences vs FLOWv4:

      • Reward asset is USDG (an ERC-20), not native ETH. fundBaskets pulls USDG; claim/auto-pay send USDG.
      • NO forfeit. Your basket is always yours.
      • Claim ANYTIME without burning (soft-claim), keeping your tokens.
      • Selling AUTO-PAYS your entire basket in the same tx (any sell — even 1 token — pays the whole basket).
        CEI + a fallback: if the USDG send fails, the amount is credited to `pendingClaim` (withdraw() later),
        so a sell can never brick.
      • Plain wallet-to-wallet transfers CARRY the proportional basket (moving between your own wallets is free).

    Pool-dependent mechanics (buyback&burn, LP auto-add) live in the v3 hook/router, not here — this contract
    is pure reward accounting + a plain ERC-20, so it needs no live pool and tests in isolation.

    The per-holder payout ASSET (USDG vs a tokenized stock) is chosen at claim time by the StockRouter layer;
    this token always denominates + pays in USDG.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface ICurve {
    function token() external view returns (address);
}

contract FlowTokenV3 is ERC20, Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    uint256 private constant MAGNITUDE = 2 ** 128;
    uint256 public constant BPS = 10_000;

    IERC20 public immutable usdg; // the reward/base asset (USDG stablecoin)

    // ── reward accounting (USDG baskets, MasterChef accumulator) ──
    uint256 public magnifiedRewardPerShare;
    mapping(address => uint256) public rewardCheckpoint;
    mapping(address => uint256) public basket;        // crystallised USDG owed
    mapping(address => uint256) private _shares;
    uint256 public totalShares;
    mapping(address => bool) public isExcludedFromRewards;
    mapping(address => uint256) public pendingClaim;  // USDG that couldn't be auto-sent → withdraw()

    // ── config ──
    address public treasury;
    address public hook;   // authorised to fund baskets with the holder-bucket USDG
    address public curve;  // launcher (single-sided seller / seeder); acts market-side
    // V4 singletons that hold the token transiently — marked market-side at construction so a restricted
    // launchpad token needs no post-deploy setAMMPair (mirrors FLOWv4). May be 0 for a standalone token.
    address public immutable poolManager;
    address public immutable positionManager;
    address public immutable universalRouter;
    bool public immutable restricted; // launchpad token → accounting setters locked (audit M-1)
    uint256 public initialSupply;
    bool public graduated; // "live on the pool"

    mapping(address => bool) public isAMMPair;         // pool side: transfer FROM = buy, TO = sell
    mapping(address => bool) public isExcludedFromFees;// pre-launch privileged movers
    mapping(address => bool) public isExcludedFromLimits;

    bool public tradingEnabled;
    uint256 public maxTx;
    uint256 public maxWallet;

    mapping(address => uint256) public holdSince; // display-only hold time

    event BasketFilled(uint256 usdgAdded, uint256 newPerShare);
    event Claimed(address indexed who, uint256 usdg);
    event AutoPaidOnSell(address indexed who, uint256 usdg);
    event BasketMoved(address indexed from, address indexed to, uint256 amount);
    event PendingCredited(address indexed who, uint256 usdg);
    event TradingEnabled();
    event HookSet(address hook);

    constructor(
        string memory name_,
        string memory symbol_,
        address treasury_,
        uint256 supply_,        // whole tokens
        address usdg_,
        bool restricted_,
        address poolManager_,       // V4 singletons (may be 0 for a standalone token)
        address positionManager_,
        address universalRouter_
    ) ERC20(name_, symbol_) Ownable(msg.sender) {
        require(treasury_ != address(0) && usdg_ != address(0), "zero");
        restricted = restricted_;
        treasury = treasury_;
        usdg = IERC20(usdg_);

        uint256 total = supply_ * 10 ** decimals();
        initialSupply = total;

        _excludeFromRewards(address(this), true);
        _excludeFromRewards(treasury_, true);
        _excludeFromRewards(msg.sender, true);
        _excludeFromRewards(address(0xdead), true);

        isExcludedFromFees[msg.sender] = true;
        isExcludedFromFees[address(this)] = true;
        isExcludedFromLimits[msg.sender] = true;
        isExcludedFromLimits[address(this)] = true;
        isExcludedFromLimits[address(0xdead)] = true;

        // On a launchpad token the treasury IS the untrusted creator → do NOT limit-exempt it (audit).
        if (!restricted_) {
            isExcludedFromFees[treasury_] = true;
            isExcludedFromLimits[treasury_] = true;
        }

        maxTx = total / 100;    // 1%
        maxWallet = total / 50; // 2%

        poolManager = poolManager_;
        positionManager = positionManager_;
        universalRouter = universalRouter_;
        if (poolManager_ != address(0)) _markInfrastructure(poolManager_);
        if (positionManager_ != address(0)) _markInfrastructure(positionManager_);
        if (universalRouter_ != address(0)) _markInfrastructure(universalRouter_);

        _mint(msg.sender, total);
    }

    // ══════════════════════════ core transfer hook ══════════════════════════
    function _update(address from, address to, uint256 amount) internal override {
        // mint / burn: keep baskets + shares in sync, no forfeit/auto-pay
        if (from == address(0) || to == address(0)) {
            if (from != address(0)) _settle(from);
            if (to != address(0)) _settle(to);
            super._update(from, to, amount);
            _syncShares(from);
            _syncShares(to);
            if (to != address(0)) _touchHoldSinceOnReceive(to, amount);
            if (from != address(0)) holdSince[from] = block.timestamp;
            return;
        }

        bool isBuy = isAMMPair[from];

        if (!tradingEnabled) {
            require(isExcludedFromFees[from] || isExcludedFromFees[to], "trading disabled");
        }
        if (tradingEnabled) {
            if (!isExcludedFromLimits[from] && !isExcludedFromLimits[to]) require(amount <= maxTx, "max tx");
            if (!isExcludedFromLimits[to] && !isAMMPair[to]) require(balanceOf(to) + amount <= maxWallet, "max wallet");
        }

        // outgoing move by an eligible holder: SELL/exit → auto-pay the whole basket; plain move → carry it
        if (!isBuy && !isExcludedFromRewards[from]) {
            _settle(from);
            if (isAMMPair[to] || isExcludedFromRewards[to]) {
                _autoPayOnSell(from);          // sell (or move to an excluded addr): realise the whole basket
            } else {
                _settle(to);
                _moveBasket(from, to, amount); // wallet-to-wallet: basket travels with the tokens
            }
        }

        _settle(from);
        _settle(to);
        super._update(from, to, amount);
        _syncShares(from);
        _syncShares(to);

        holdSince[from] = block.timestamp;
        _touchHoldSinceOnReceive(to, amount);
    }

    // ══════════════════════════ reward internals ══════════════════════════
    function _settle(address account) internal {
        if (isExcludedFromRewards[account]) { rewardCheckpoint[account] = magnifiedRewardPerShare; return; }
        uint256 m = magnifiedRewardPerShare;
        uint256 delta = m - rewardCheckpoint[account];
        if (delta > 0) {
            uint256 accrued = (delta * balanceOf(account)) / MAGNITUDE;
            if (accrued > 0) basket[account] += accrued;
            rewardCheckpoint[account] = m;
        }
    }

    function _moveBasket(address from, address to, uint256 amount) internal {
        uint256 bal = balanceOf(from);
        if (bal == 0) return;
        uint256 b = basket[from];
        if (b == 0) return;
        uint256 moveAmt = (b * amount) / bal;
        if (moveAmt == 0) return;
        basket[from] = b - moveAmt;
        basket[to] += moveAmt;
        emit BasketMoved(from, to, moveAmt);
    }

    /// On a sell (or move to an excluded addr): pay the seller's ENTIRE basket in USDG, same tx. CEI; on a
    /// failed send the amount is credited to pendingClaim so the sell never reverts.
    function _autoPayOnSell(address from) internal {
        uint256 owed = basket[from];
        if (owed == 0) return;
        basket[from] = 0; // effect first
        _payUsdg(from, owed, true);
        emit AutoPaidOnSell(from, owed);
    }

    /// Send USDG best-effort; credit pendingClaim on failure (so an in-swap auto-pay can't brick).
    function _payUsdg(address to, uint256 amt, bool creditOnFail) internal {
        if (amt == 0) return;
        if (usdg.balanceOf(address(this)) < amt) { // shouldn't happen if funded correctly; be safe
            if (creditOnFail) { pendingClaim[to] += amt; emit PendingCredited(to, amt); }
            return;
        }
        (bool ok, bytes memory ret) = address(usdg).call(abi.encodeWithSelector(IERC20.transfer.selector, to, amt));
        bool success = ok && (ret.length == 0 || abi.decode(ret, (bool)));
        if (!success && creditOnFail) { pendingClaim[to] += amt; emit PendingCredited(to, amt); }
        else if (!success) revert("usdg transfer");
    }

    function _syncShares(address account) internal {
        if (account == address(0)) return;
        uint256 newS = isExcludedFromRewards[account] ? 0 : balanceOf(account);
        uint256 old = _shares[account];
        if (newS != old) { totalShares = totalShares - old + newS; _shares[account] = newS; }
    }

    function _touchHoldSinceOnReceive(address to, uint256 received) internal {
        if (received == 0 || isExcludedFromRewards[to]) return;
        uint256 newBal = balanceOf(to);
        uint256 oldBal = newBal > received ? newBal - received : 0;
        holdSince[to] = oldBal == 0 ? block.timestamp
            : (oldBal * holdSince[to] + received * block.timestamp) / newBal;
    }

    // ══════════════════════════ claim (soft, no burn) ══════════════════════════
    /// Withdraw your accrued basket in USDG WITHOUT burning — keep your tokens, keep earning.
    function claim() external nonReentrant {
        _settle(msg.sender);
        uint256 owed = basket[msg.sender];
        require(owed > 0, "nothing");
        basket[msg.sender] = 0;
        _payUsdg(msg.sender, owed, false); // direct claim: revert on failure (no in-swap context)
        emit Claimed(msg.sender, owed);
    }

    /// Withdraw any USDG that a failed auto-pay credited to you.
    function withdrawPending() external nonReentrant {
        uint256 amt = pendingClaim[msg.sender];
        require(amt > 0, "nothing");
        pendingClaim[msg.sender] = 0;
        _payUsdg(msg.sender, amt, false);
    }

    // ══════════════════════════ USDG in → baskets ══════════════════════════
    /// The hook (holder bucket), the curve, or the owner deposits USDG here → baskets. Caller must approve.
    function fundBaskets(uint256 amount) external {
        require(msg.sender == curve || msg.sender == hook || msg.sender == owner(), "auth");
        if (amount == 0) return;
        usdg.safeTransferFrom(msg.sender, address(this), amount);
        if (totalShares > 0) {
            magnifiedRewardPerShare += (amount * MAGNITUDE) / totalShares;
            emit BasketFilled(amount, magnifiedRewardPerShare);
        } else {
            pendingClaim[treasury] += amount; // no holders yet → don't strand it (goes to project treasury)
        }
    }

    // ══════════════════════════ views ══════════════════════════
    function basketOf(address account) public view returns (uint256) {
        if (isExcludedFromRewards[account]) return basket[account];
        uint256 delta = magnifiedRewardPerShare - rewardCheckpoint[account];
        return basket[account] + (delta * balanceOf(account)) / MAGNITUDE;
    }

    function burned() external view returns (uint256) {
        return initialSupply - totalSupply() + balanceOf(address(0xdead));
    }

    function holdDuration(address account) external view returns (uint256) {
        uint256 hs = holdSince[account];
        return hs == 0 ? 0 : block.timestamp - hs;
    }

    // ══════════════════════════ admin ══════════════════════════
    function enableTrading() external onlyOwner {
        require(!tradingEnabled, "already");
        require(curve == address(0) || graduated, "use curve");
        tradingEnabled = true;
        emit TradingEnabled();
    }

    function setHook(address h) external onlyOwner {
        require(hook == address(0) && h != address(0), "hook set");
        hook = h;
        _markInfrastructure(h);
        emit HookSet(h);
    }

    function setCurve(address c) external onlyOwner {
        require(curve == address(0) && c != address(0), "curve set");
        require(!tradingEnabled, "already live");
        require(ICurve(c).token() == address(this), "bad curve");
        curve = c;
        _markInfrastructure(c);
        isExcludedFromFees[c] = true;
        isExcludedFromLimits[c] = true;
    }

    function onGraduate() external {
        require(msg.sender == curve, "auth");
        graduated = true;
        if (!tradingEnabled) { tradingEnabled = true; emit TradingEnabled(); }
    }

    function removeLimits() external onlyOwner {
        maxTx = type(uint256).max;
        maxWallet = type(uint256).max;
    }

    function setLimits(uint256 maxTx_, uint256 maxWallet_) external onlyOwner notRestricted {
        require(maxTx_ >= totalSupply() / 1000 && maxWallet_ >= totalSupply() / 1000, "too low");
        maxTx = maxTx_; maxWallet = maxWallet_;
    }

    function setAMMPair(address p, bool v) external onlyOwner notRestricted {
        if (!v) require(!_isProtected(p), "protected");
        isAMMPair[p] = v;
        _settle(p);
        _excludeFromRewards(p, v);
        _syncShares(p);
    }

    function setExcludedFromFees(address a, bool v) external onlyOwner notRestricted { isExcludedFromFees[a] = v; }
    function setExcludedFromLimits(address a, bool v) external onlyOwner notRestricted { isExcludedFromLimits[a] = v; }

    function setExcludedFromRewards(address a, bool v) external onlyOwner notRestricted {
        if (!v) require(!_isProtected(a), "protected");
        _settle(a);
        _excludeFromRewards(a, v);
        _syncShares(a);
    }

    function setTreasury(address t) external onlyOwner {
        require(t != address(0), "zero");
        treasury = t;
    }

    // mark a pool/router/curve as market-side: never accrues, TO = sell, FROM = buy, maxWallet-exempt
    function _markInfrastructure(address a) internal {
        _settle(a);
        isAMMPair[a] = true;
        _excludeFromRewards(a, true);
        _syncShares(a);
    }

    function _isProtected(address a) internal view returns (bool) {
        return a == address(this) || a == hook || a == curve || a == address(0xdead)
            || (a != address(0) && (a == poolManager || a == positionManager || a == universalRouter));
    }

    function _excludeFromRewards(address a, bool v) internal {
        isExcludedFromRewards[a] = v;
        rewardCheckpoint[a] = magnifiedRewardPerShare;
    }

    modifier notRestricted() { require(!restricted, "restricted"); _; }
}
Chain explorer1931msChain node88ms