FLOWv4

0x856e1c2db4ae8b31f698fb052432a560ec0407f7

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
12,738 bytes
ABI entries
84
40 read · 23 write
License
none

Contract information

Address
0x856e1c2db4ae8b31f698fb052432a560ec0407f7
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xe73daa06f6…7ba646873e

Token

Name
FLOW Backwards
Symbol
WOLF
Decimals
18
Holders
19

Read contract (40)

BPS()uint256
BUYBACK_MAX_SLIP_BPS()uint256
FORFEIT_LP_BPS()uint256
FORFEIT_PROJECT_BPS()uint256
POOL_FEE()uint24
POOL_TICK_SPACING()int24
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
pendingLiquidity()uint256
pendingProject()uint256
platformVault()address
poolManager()address
positionManager()address
restricted()bool
rewardCheckpoint(address)uint256
symbol()string
totalShares()uint256
totalSupply()uint256
tradingEnabled()bool
treasury()address
universalRouter()address

Events (9)

ApprovalBasketFilledBasketMovedForfeitedHookSetOwnershipTransferredRedeemedTradingEnabledTransfer

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": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "universalRouter_",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "restricted_",
        "type": "bool"
      }
    ],
    "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": false,
        "internalType": "uint256",
        "name": "nativeAdded",
        "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": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "toProject",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "toLp",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "toHolders",
        "type": "uint256"
      }
    ],
    "name": "Forfeited",
    "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": "tokensBurned",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "nativePaid",
        "type": "uint256"
      }
    ],
    "name": "Redeemed",
    "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": [],
    "name": "BUYBACK_MAX_SLIP_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FORFEIT_LP_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FORFEIT_PROJECT_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POOL_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POOL_TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "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": "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": [],
    "name": "fundBaskets",
    "outputs": [],
    "stateMutability": "payable",
    "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": [],
    "name": "pendingLiquidity",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingProject",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "platformVault",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "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": "processLiquidity",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "processProject",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "n",
        "type": "uint256"
      }
    ],
    "name": "redeem",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "redeemAll",
    "outputs": [],
    "stateMutability": "nonpayable",
    "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": "v",
        "type": "address"
      }
    ],
    "name": "setPlatformVault",
    "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": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
// ^0.8.24 (not fixed 0.8.26): this token uses no V4 features, so it compiles under BOTH
// Hardhat (0.8.24 — lets us unit-test it now) and Foundry (0.8.26 — for the hook stack later).
pragma solidity ^0.8.24;

/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    FLOW (V4 route) — earn while you hold.

    ┌─────────────────────────────────────────────────────────────────────────┐
    │  ⚠️  WORK IN PROGRESS — V4 migration. NOT AUDITED. This is the "plain      │
    │      ERC-20" token for the Uniswap-V4-hook launch: the 5% trade tax lives  │
    │      in FlowHook.sol (step 1), NOT in this token. See                      │
    │      docs/V4-HOOK-MIGRATION.md. Named `FLOWv4` during the migration so it   │
    │      doesn't collide with the audited V2 `FLOW`; rename to `FLOW` on cut-over.│
    └─────────────────────────────────────────────────────────────────────────┘

    What CHANGED vs the V2 token:
      • NO fee-on-transfer tax in _update. On V4 the tax is a swap-fee taken by the
        hook (in ETH) and forwarded here via fundBaskets() — same accumulator, same
        baskets. Removed: _swapBack / manualSwapBack / autoSwap / swapThreshold /
        maxSwapBack / inSwap, and the V2 router+pair plumbing.
      • Exclusions retarget from the V2 `pair` to the V4 singletons
        (PoolManager / PositionManager / UniversalRouter): they hold FLOW transiently
        during swaps/liquidity and must not accrue baskets or forfeit.

    What STAYED the same (economics unchanged):
      • Baskets: native (ETH) streamed pro-rata by balance via the O(1) accumulator.
      • Forfeit: any outgoing move (sell OR wallet transfer) forfeits a PROPORTIONAL
        slice — 20% project · 40% buyback+burn · 40% holders. Touches ONLY basket
        accounting, not token amounts, so it works fine on a plain ERC-20.
      • redeem(n): burn n FLOW → withdraw n/balance of your basket (native), CEI + guard.
      • Bonding-curve phase is IDENTICAL: the curve charges its 5% in native and calls
        fundBaskets() (no DEX involved). V4 only appears at graduation (LaunchCurve).
      • holdSince[wallet]: display-only hold-time.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */

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 {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {CurrencySettler} from "@openzeppelin/uniswap-hooks/src/utils/CurrencySettler.sol";

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

contract FLOWv4 is ERC20, Ownable, ReentrancyGuard, IUnlockCallback {
    using CurrencySettler for Currency;
    using BalanceDeltaLibrary for BalanceDelta;
    using StateLibrary for IPoolManager;
    using PoolIdLibrary for PoolKey;

    // ───────────────────────────── constants ─────────────────────────────
    uint256 private constant MAGNITUDE = 2 ** 128; // fixed-point scalar for the accumulator
    uint256 public constant BPS = 10_000;
    uint24 public constant POOL_FEE = 3000;        // must match LaunchCurveV4.LP_FEE
    int24 public constant POOL_TICK_SPACING = 60;  // must match LaunchCurveV4.TICK_SPACING

    // forfeit split (of a forfeited basket): 20% project / 40% buyback / remainder(40%) holders.
    // (No cross-token "platform" slice — FLOW earns from the ecosystem via the hook's 0.25% trade cut.)
    uint256 public constant FORFEIT_PROJECT_BPS = 2_000;  // 20% project treasury
    uint256 public constant FORFEIT_LP_BPS = 4_000;       // 40% buyback + burn
    // remainder (40%) spills to other holders

    // ───────────────────────────── reward accounting ─────────────────────
    // Basket = native token accrued to a holder. MasterChef-style accumulator:
    // `magnifiedRewardPerShare` only moves when native is added; each account
    // crystallises its pending accrual into `basket[account]` (via _settle) before
    // any balance change.
    uint256 public magnifiedRewardPerShare;
    mapping(address => uint256) public rewardCheckpoint; // last seen magnifiedRewardPerShare
    mapping(address => uint256) public basket;           // crystallised native owed
    mapping(address => uint256) private _shares;         // mirror of eligible balance
    uint256 public totalShares;                          // Σ eligible balances

    mapping(address => bool) public isExcludedFromRewards;

    // ───────────────────────────── forfeit reserves ──────────────────────
    uint256 public pendingProject;   // native queued for the project treasury
    uint256 public pendingLiquidity; // native queued for buyback + burn

    // ───────────────────────────── hold-time (display only) ──────────────
    mapping(address => uint256) public holdSince; // weighted-avg acquire time; reset on any move-out

    // ───────────────────────────── trading config ────────────────────────
    address public treasury;

    // ── V4 infrastructure (singletons that hold FLOW transiently) ──
    address public immutable poolManager;
    address public immutable positionManager;
    address public immutable universalRouter;
    address public hook; // FlowHook — forwards the 5% swap fee (native) to fundBaskets()
    address public platformVault; // (FLOW only) redeems the 2%-positions it holds → ETH into FLOW baskets

    // Launchpad tokens are deployed `restricted`: their creator gets ownership but the
    // reward/forfeit-accounting setters are permanently locked (audit M-1), so a creator can't
    // manipulate their token's baskets to harm buyers. FLOW itself (the flagship) is unrestricted.
    bool public immutable restricted;

    uint256 public initialSupply; // the amount minted at launch (for a live "% burned" readout)

    // ── bonding-curve launch ──
    address public curve;  // LaunchCurve contract (phase-1 seller); 0 for a direct DEX launch
    bool public graduated; // true once the curve has seeded the V4 pool

    // Addresses that behave like a market (the pool side): transfers FROM them are
    // "buys" (no forfeit on the sender), and they escape maxWallet.
    mapping(address => bool) public isAMMPair;
    // Pre-launch "privileged mover" allowlist (owner seeds the curve while trading is off).
    mapping(address => bool) public isExcludedFromFees;
    mapping(address => bool) public isExcludedFromLimits;

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

    // ───────────────────────────── events ────────────────────────────────
    event BasketFilled(uint256 nativeAdded, uint256 newPerShare);
    event Forfeited(address indexed who, uint256 amount, uint256 toProject, uint256 toLp, uint256 toHolders);
    event BasketMoved(address indexed from, address indexed to, uint256 amount); // plain transfer carries the basket
    event Redeemed(address indexed who, uint256 tokensBurned, uint256 nativePaid);
    event TradingEnabled();
    event HookSet(address hook);

    constructor(
        string memory name_,       // per-launch name/symbol (the launchpad picks these)
        string memory symbol_,
        address treasury_,
        uint256 supply_,           // whole tokens, e.g. 1_000_000_000
        address poolManager_,
        address positionManager_,
        address universalRouter_,
        bool restricted_           // true for launchpad tokens (locks accounting setters)
    ) ERC20(name_, symbol_) Ownable(msg.sender) {
        restricted = restricted_;
        require(treasury_ != address(0), "zero addr");
        require(poolManager_ != address(0) && positionManager_ != address(0) && universalRouter_ != address(0), "zero v4");
        treasury = treasury_;
        poolManager = poolManager_;
        positionManager = positionManager_;
        universalRouter = universalRouter_;

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

        // The V4 singletons hold FLOW transiently during swaps/liquidity — treat them like
        // the market side: excluded from rewards (never accrue baskets), maxWallet-exempt (via
        // isAMMPair), and a transfer FROM them (a buy) doesn't forfeit the recipient.
        _markInfrastructure(poolManager_);
        _markInfrastructure(positionManager_);
        _markInfrastructure(universalRouter_);

        // never-earn / never-forfeit set
        _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 (restricted) token the treasury IS the untrusted creator. Exempting it from
        // limits would let the creator snipe their own launch past the advertised maxTx/maxWallet caps
        // — and that exemption is un-removable (setters are notRestricted-locked). So only the FLAGSHIP
        // (unrestricted, treasury = us) gets the treasury fee/limit exemption. (audit: creator snipe edge)
        if (!restricted_) {
            isExcludedFromFees[treasury_] = true;
            isExcludedFromLimits[treasury_] = true;
        }

        // launch limits (owner can loosen after launch)
        maxTx = total / 100;      // 1%
        maxWallet = total / 50;   // 2%

        _mint(msg.sender, total); // owner seeds the curve from here
    }

    receive() external payable {} // accept native (reward reserve) from the hook / curve

    /// Mark a V4 singleton as market-side infrastructure: never accrues baskets, a transfer
    /// FROM it is a "buy" (no forfeit on the recipient), and it escapes maxWallet via isAMMPair.
    /// Deliberately NOT limit-exempt, so per-tx maxTx still applies to trades (mirrors the V2
    /// pair, which was maxWallet-exempt but maxTx-enforced). The curve/owner big transfers are
    /// exempt via their own isExcludedFromLimits, so graduation seeding isn't blocked.
    function _markInfrastructure(address a) internal {
        _settle(a); // crystallise + resync like every other exclusion setter, so totalShares stays
        isAMMPair[a] = true; // == Σ eligible balances even if `a` already held FLOW (audit L-1)
        _excludeFromRewards(a, true);
        _syncShares(a);
    }

    // ══════════════════════════════════════════════════════════════════════
    //                          CORE TRANSFER HOOK
    // ══════════════════════════════════════════════════════════════════════
    function _update(address from, address to, uint256 amount) internal override {
        // Mint / burn: no forfeit — just keep baskets & shares in sync.
        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; // burn = exit → reset streak
            return;
        }

        bool isBuy = isAMMPair[from]; // pool → holder

        // ── launch guards ──
        // Pre-launch, only privileged (fee-exempt) addresses may move tokens — lets the
        // owner seed the curve while buys/sells stay blocked.
        if (!tradingEnabled) {
            require(isExcludedFromFees[from] || isExcludedFromFees[to], "trading disabled");
        }
        // Token-level limits apply only after launch. During the curve phase the LaunchCurve
        // enforces its own per-wallet cap.
        if (tradingEnabled) {
            if (!isExcludedFromLimits[from] && !isExcludedFromLimits[to]) {
                require(amount <= maxTx, "max tx");
            }
            if (!isExcludedFromLimits[to] && !isAMMPair[to]) {
                require(balanceOf(to) + amount <= maxWallet, "max wallet");
            }
        }

        // ── EXIT (forfeit) vs MOVE (basket travels with the tokens) ──
        // A holder's outgoing move either EXITS its basket or CARRIES it along. It's an exit — forfeit
        // + redistribute — when the tokens go to the market (a sell: isAMMPair[to]) or to a
        // reward-excluded address (where a basket can't live). A plain transfer to another eligible
        // holder instead MOVES the proportional basket to the recipient, so moving between your own
        // wallets never costs you. Selling still forfeits and can't be dodged: the basket travels to the
        // new wallet and forfeits on the real sell. Runs BEFORE we settle `to` (forfeit can bump the accumulator).
        if (!isBuy && !isExcludedFromRewards[from]) {
            _settle(from);
            if (isAMMPair[to] || isExcludedFromRewards[to]) {
                _forfeit(from, amount);        // sell, or a move to an excluded address
            } else {
                _settle(to);
                _moveBasket(from, to, amount); // plain wallet-to-wallet transfer
            }
        }

        // Settle both parties at the (possibly bumped) accumulator, on their pre-change
        // balances, so the spill lands correctly.
        _settle(from);
        _settle(to);

        // No tax here — the 5% is taken by FlowHook on swaps and pushed to fundBaskets().
        super._update(from, to, amount);

        _syncShares(from);
        _syncShares(to);

        // ── hold-time (display only) ──
        holdSince[from] = block.timestamp;          // any outgoing move breaks the streak
        _touchHoldSinceOnReceive(to, amount);       // weighted-avg for the receiver
    }

    // ══════════════════════════════════════════════════════════════════════
    //                        REWARD / BASKET INTERNALS
    // ══════════════════════════════════════════════════════════════════════

    /// Crystallise pending accrual into basket[account]; advance its checkpoint.
    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;
        }
    }

    /// Move a proportional slice of `from`'s basket to `to` on a plain transfer — the basket travels
    /// with the tokens, so moving between your own wallets costs nothing. Assumes BOTH were just
    /// _settle'd (so basket[] holds each side's crystallised value and no pending accrual is skipped).
    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; // proportional to the fraction leaving
        if (moveAmt == 0) return;
        basket[from] = b - moveAmt;
        basket[to] += moveAmt;
        emit BasketMoved(from, to, moveAmt);
    }

    /// Forfeit a proportional slice of `from`'s basket when `amount` tokens leave.
    /// Assumes `from` was just _settle'd. Splits 20% project / 40% buyback / 40% holders,
    /// and excludes `from` from its own spill.
    function _forfeit(address from, uint256 amount) internal {
        uint256 bal = balanceOf(from);
        if (bal == 0) return;
        uint256 b = basket[from];
        if (b == 0) return;

        uint256 forfeitAmt = (b * amount) / bal; // proportional to fraction leaving
        if (forfeitAmt == 0) return;
        basket[from] = b - forfeitAmt;

        uint256 toProject = (forfeitAmt * FORFEIT_PROJECT_BPS) / BPS;
        uint256 toLp = (forfeitAmt * FORFEIT_LP_BPS) / BPS;
        uint256 toHolders = forfeitAmt - toProject - toLp;

        pendingProject += toProject;
        // The buyback (processLiquidity) can only spend pendingLiquidity once the V4 pool is live (graduated).
        // In production `graduated` is set at launch, so this is always the pendingLiquidity path. If a token
        // ever forfeits before a pool exists (no buyback venue), route the slice to the project queue instead
        // of stranding it in an unspendable one — same spirit as the "no other holders" fallback below.
        if (graduated) pendingLiquidity += toLp;
        else pendingProject += toLp;

        // Spill the holders' slice across OTHER holders. Distribute over
        // (totalShares − from's shares); advance from's checkpoint to exclude it.
        uint256 eligible = totalShares > bal ? totalShares - bal : 0;
        if (toHolders > 0 && eligible > 0) {
            magnifiedRewardPerShare += (toHolders * MAGNITUDE) / eligible;
            rewardCheckpoint[from] = magnifiedRewardPerShare; // exclude `from`
        } else if (toHolders > 0) {
            // no other holders → route to project rather than stranding it
            pendingProject += toHolders;
        }

        emit Forfeited(from, forfeitAmt, toProject, toLp, toHolders);
    }

    /// Keep totalShares == Σ balances of reward-eligible accounts.
    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;
        }
    }

    /// Weighted-average acquire time so buying more only gently lowers the streak.
    function _touchHoldSinceOnReceive(address to, uint256 received) internal {
        if (received == 0 || isExcludedFromRewards[to]) return;
        uint256 newBal = balanceOf(to);
        uint256 oldBal = newBal > received ? newBal - received : 0;
        if (oldBal == 0) {
            holdSince[to] = block.timestamp;
        } else {
            holdSince[to] = (oldBal * holdSince[to] + received * block.timestamp) / newBal;
        }
    }

    // ══════════════════════════════════════════════════════════════════════
    //                                REDEEM
    // ══════════════════════════════════════════════════════════════════════
    /// Burn `n` FLOW and withdraw the matching slice of your basket (native).
    /// Exempt from the forfeit rule. Native is paid LAST (CEI) + nonReentrant.
    function redeem(uint256 n) external nonReentrant {
        _redeem(n);
    }

    /// Redeem your entire position.
    function redeemAll() external nonReentrant {
        _redeem(balanceOf(msg.sender));
    }

    function _redeem(uint256 n) internal {
        _settle(msg.sender);
        uint256 bal = balanceOf(msg.sender);
        require(n > 0 && n <= bal, "bad amount");

        uint256 owed = (basket[msg.sender] * n) / bal;
        basket[msg.sender] -= owed;          // effect
        _burn(msg.sender, n);                // effect (triggers _update burn branch)

        if (owed > 0) {                      // interaction — LAST
            (bool ok, ) = payable(msg.sender).call{value: owed}("");
            require(ok, "native transfer failed");
        }
        emit Redeemed(msg.sender, n, owed);
    }

    // ══════════════════════════════════════════════════════════════════════
    //                    NATIVE IN → BASKETS (hook / curve)
    // ══════════════════════════════════════════════════════════════════════
    /// The curve (phase 1), the hook (post-graduation), and the platform vault (redeemed cross-token
    /// baskets) deposit native here → baskets.
    function fundBaskets() external payable {
        require(msg.sender == curve || msg.sender == hook || msg.sender == platformVault || msg.sender == owner(), "auth");
        if (msg.value == 0) return;
        if (totalShares > 0) {
            magnifiedRewardPerShare += (msg.value * MAGNITUDE) / totalShares;
            emit BasketFilled(msg.value, magnifiedRewardPerShare);
        } else {
            pendingProject += msg.value; // no holders yet → don't strand it
        }
    }

    // ══════════════════════════════════════════════════════════════════════
    //                    FORFEIT-FUNDS PROCESSING (keeper)
    // ══════════════════════════════════════════════════════════════════════
    /// Send queued project funds to the treasury.
    function processProject() external nonReentrant {
        uint256 amt = pendingProject;
        require(amt > 0, "nothing");
        pendingProject = 0;
        (bool ok, ) = payable(treasury).call{value: amt}("");
        require(ok, "send failed");
    }

    // Buyback per-call price-impact bound (on the pool's sqrtPrice). Caps how far one buyback can push
    // the price, so the unspent ETH carries over — this bounds any single sandwich's extractable value
    // and, combined with the call being PERMISSIONLESS, removes the owner's exclusive timing advantage.
    uint256 public constant BUYBACK_MAX_SLIP_BPS = 250; // ~5% price move max per call

    /// Buyback-and-burn the forfeit "liquidity" slice: swap `pendingLiquidity` ETH for FLOW on the V4
    /// pool and burn it → drops supply, supports the floor. PERMISSIONLESS (anyone/any keeper may call;
    /// the owner has no special power here — audit M-1). The swap is hard-bounded to
    /// BUYBACK_MAX_SLIP_BPS off the pool's CURRENT price via an on-chain slot0 read; ETH not spent
    /// within that bound is returned to pendingLiquidity for the next call. (The buyback itself pays the
    /// hook fee, so a little flows to baskets; the rest buys & burns.)
    function processLiquidity() external nonReentrant {
        require(graduated, "not live"); // needs the V4 pool
        uint256 amt = pendingLiquidity;
        require(amt > 0, "nothing");
        pendingLiquidity = 0;
        IPoolManager(poolManager).unlock(abi.encode(amt));
    }

    /// PoolManager unlock callback for the buyback: swap ETH → FLOW (price-bounded), then burn it.
    function unlockCallback(bytes calldata data) external override returns (bytes memory) {
        require(msg.sender == poolManager, "auth");
        uint256 amt = abi.decode(data, (uint256));

        PoolKey memory key = _poolKey();
        // bound the swap to BUYBACK_MAX_SLIP_BPS below the current price (ETH→FLOW lowers the price);
        // getSlot0 is a live on-chain read of the pool's sqrtPrice.
        // ACCEPTED RISK (audit M-2): the reference is the instantaneous slot0 price, so a same-block MEV
        // manipulator can push the price up and have the buyback execute near it, leaking BOUNDED value
        // (≤ BUYBACK_MAX_SLIP_BPS per call) from the buyback queue only — never holder baskets or principal,
        // and unspent ETH carries over (below). A manipulation-resistant TWAP/oracle reference is tracked as
        // dedicated future hardening; the per-call bound is the interim protection.
        (uint160 sqrtNow, , , ) = IPoolManager(poolManager).getSlot0(key.toId());
        uint160 limit = uint160((uint256(sqrtNow) * (BPS - BUYBACK_MAX_SLIP_BPS)) / BPS);
        if (limit <= TickMath.MIN_SQRT_PRICE) limit = TickMath.MIN_SQRT_PRICE + 1;

        BalanceDelta delta = IPoolManager(poolManager).swap(
            key,
            SwapParams({ zeroForOne: true, amountSpecified: -int256(amt), sqrtPriceLimitX96: limit }),
            ""
        );

        int128 d0 = delta.amount0(); // ETH we owe (negative)
        int128 d1 = delta.amount1(); // FLOW we receive (positive)
        uint256 ethUsed = d0 < 0 ? uint256(uint128(-d0)) : 0;
        if (ethUsed > 0) key.currency0.settle(IPoolManager(poolManager), address(this), ethUsed, false);
        // carry over ETH the price-bound left unspent, so it isn't stranded and backs the next buyback
        if (ethUsed < amt) pendingLiquidity += (amt - ethUsed);

        uint256 flowOut = d1 > 0 ? uint256(uint128(d1)) : 0;
        if (flowOut > 0) {
            key.currency1.take(IPoolManager(poolManager), address(this), flowOut, false); // pull FLOW here
            _burn(address(this), flowOut);                                                // real supply burn
        }
        return "";
    }

    function _poolKey() internal view returns (PoolKey memory) {
        return PoolKey({
            currency0: Currency.wrap(address(0)), // native ETH
            currency1: Currency.wrap(address(this)),
            fee: POOL_FEE,
            tickSpacing: POOL_TICK_SPACING,
            hooks: IHooks(hook)
        });
    }

    // ══════════════════════════════════════════════════════════════════════
    //                              VIEWS
    // ══════════════════════════════════════════════════════════════════════
    /// Tokens removed from circulation: real supply burns (redeem + buyback) plus the graduation
    /// remainder parked at the dead address. For a live "X burned / Y% of supply" readout.
    function burned() external view returns (uint256) {
        return initialSupply - totalSupply() + balanceOf(address(0xdead));
    }

    /// Live basket value (crystallised + pending accrual) in native wei.
    function basketOf(address account) public view returns (uint256) {
        if (isExcludedFromRewards[account]) return basket[account];
        uint256 delta = magnifiedRewardPerShare - rewardCheckpoint[account];
        uint256 pending = (delta * balanceOf(account)) / MAGNITUDE;
        return basket[account] + pending;
    }

    /// Seconds the wallet has held its current position (0 after any move-out).
    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");
        // If a curve is set, the DEX may only open via graduation — prevents opening a
        // second market before the curve seeds the V4 pool at the final price.
        require(curve == address(0) || graduated, "use curve");
        tradingEnabled = true;
        emit TradingEnabled();
    }

    /// Wire the FlowHook (authorised to fundBaskets with the swap fee). Excluded like infra.
    function setHook(address h) external onlyOwner {
        require(hook == address(0), "hook set"); // one-shot: can't swap the hook post-launch (audit M-2)
        require(h != address(0), "zero");
        hook = h;
        _markInfrastructure(h);
        emit HookSet(h);
    }

    /// One-shot: authorise the PlatformVault to stream redeemed cross-token baskets into FLOW baskets.
    /// (Only meaningful on the FLOW platform token.)
    function setPlatformVault(address v) external onlyOwner notRestricted {
        require(platformVault == address(0) && v != address(0), "set");
        platformVault = v;
    }

    // ── bonding-curve wiring ──
    /// Register the LaunchCurve. It sells FLOW in phase 1, so it acts like a market
    /// (sells forfeit) and is fee/limit exempt (its fee is charged in native).
    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"); // reject EOA/typo → no brick
        curve = c;
        isAMMPair[c] = true;
        isExcludedFromFees[c] = true;
        isExcludedFromLimits[c] = true;
        _settle(c);
        _excludeFromRewards(c, true);
        _syncShares(c);
    }

    /// Called by the curve at graduation: opens trading.
    function onGraduate() external {
        require(msg.sender == curve, "auth");
        graduated = true;
        if (!tradingEnabled) {
            tradingEnabled = true;
            emit TradingEnabled();
        }
    }

    // Restricted (launchpad) tokens can only LOOSEN limits (removeLimits), never re-tighten them —
    // so a creator can't grief holders by clamping trading after launch. (audit M-1)
    function setLimits(uint256 maxTx_, uint256 maxWallet_) external onlyOwner notRestricted {
        require(maxTx_ >= totalSupply() / 1000 && maxWallet_ >= totalSupply() / 1000, "too low");
        maxTx = maxTx_;
        maxWallet = maxWallet_;
    }

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

    /// Reward/forfeit-accounting setters below are permanently locked on `restricted` (launchpad)
    /// tokens, so a creator who receives ownership cannot manipulate baskets to harm buyers (audit M-1).
    /// The V4 singletons (PoolManager/PositionManager/UniversalRouter) are already market-side from the
    /// constructor, so a restricted token needs none of these post-launch.
    modifier notRestricted() {
        require(!restricted, "restricted");
        _;
    }

    function setAMMPair(address p, bool v) external onlyOwner notRestricted {
        require(p != poolManager, "core pool");
        if (!v) require(!_isProtected(p), "protected"); // don't re-include protected addrs
        isAMMPair[p] = v;
        _settle(p);
        _excludeFromRewards(p, v);
        _syncShares(p);
    }

    /// Addresses that must never become reward-eligible (would strand value or break the
    /// totalShares invariant if re-included via a setter).
    function _isProtected(address a) internal view returns (bool) {
        return a == address(this)
            || a == poolManager
            || a == positionManager
            || a == universalRouter
            || a == hook
            || a == curve
            || a == address(0xdead);
    }

    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"); // can't make protected addrs eligible
        _settle(a);
        _excludeFromRewards(a, v);
        _syncShares(a);
    }

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

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