HoodlLaunchController

0xa7946c7c6224ec8f8759dc864a02319fe0f69825

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
15,433 bytes
ABI entries
69
16 read · 11 write
License
none

Contract information

Address
0xa7946c7c6224ec8f8759dc864a02319fe0f69825
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0xe1caef40a7…c96c65e473

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (16)

BURN_ADDRESS()address
boughtInFairLaunch(bytes32, address)uint256
fairLaunchInfo(bytes32)tuple
hoodl()address
hook()address
inFairLaunchWindow(bytes32)bool
isFairLaunchOpen(bytes32)bool
isWhitelisted(bytes32, address, uint256, bytes32[])bool
launchPoolId()bytes32
launchPoolKeySet()bool
marketCapToPrice(uint256, uint256)uint256
owner()address
poolManager()address
quotePayment(bytes32, uint256)uint256
quoteTokensForPayment(bytes32, uint256)uint256
whitelistLeaf(address, uint256)bytes32

Events (12)

FairLaunchBuyTaxSettledFairLaunchCloseShortfallHandledFairLaunchClosedFairLaunchConfiguredFairLaunchFilledFairLaunchLiquidityCreatedFairLaunchRescuedHookSetLaunchPoolKeySetNativeWithdrawnOwnershipTransferredPoolInitialized

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "hoodl_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "poolManager_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AmountTooLarge",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadFairLaunchWindow",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadLiquidityRange",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExceedsFairLaunchSupply",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExceedsWalletCap",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExceedsWhitelistAllocation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FairLaunchAlreadyStarted",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FairLaunchClosedAlready",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FairLaunchHookLocked",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FairLaunchNotActive",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FairLaunchNotConfigured",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "startsAt",
        "type": "uint256"
      }
    ],
    "name": "FairLaunchNotStarted",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FairLaunchStillActive",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "IncorrectNativePayment",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InitialTickOutOfRange",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidFairLaunchAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidPoolHook",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidProof",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidTaxQuote",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyHook",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyNativeHoodlPool",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyPoolManager",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint160",
        "name": "actualSqrtPriceX96",
        "type": "uint160"
      },
      {
        "internalType": "int24",
        "name": "actualTick",
        "type": "int24"
      },
      {
        "internalType": "uint160",
        "name": "expectedSqrtPriceX96",
        "type": "uint160"
      },
      {
        "internalType": "int24",
        "name": "expectedTick",
        "type": "int24"
      }
    ],
    "name": "PoolPriceMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroLiquidity",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "taxBaseAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalTax",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "teamTax",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "sheriffTax",
        "type": "uint256"
      }
    ],
    "name": "FairLaunchBuyTaxSettled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "nativeRequested",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "nativeUsed",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenLiquidityRequested",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenLiquidityUsed",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "unsoldRequested",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "unsoldBurned",
        "type": "uint256"
      }
    ],
    "name": "FairLaunchCloseShortfallHandled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "revenue",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "sold",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "unsoldSupply",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenLiquiditySupply",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "endedAt",
        "type": "uint256"
      }
    ],
    "name": "FairLaunchClosed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "startsAt",
        "type": "uint64"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "endsAt",
        "type": "uint64"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "initialTick",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "pricePerToken",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fairLaunchSupply",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "maxPerWallet",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenLiquiditySupply",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "merkleRoot",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "whitelistRequired",
        "type": "bool"
      }
    ],
    "name": "FairLaunchConfigured",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "paymentAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalPayment",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "revenue",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "remainingSupply",
        "type": "uint256"
      }
    ],
    "name": "FairLaunchFilled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "tickLower",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "tickUpper",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "tokenIsCurrency0",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "liquidity",
        "type": "uint128"
      }
    ],
    "name": "FairLaunchLiquidityCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "nativeAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "FairLaunchRescued",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldHook",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newHook",
        "type": "address"
      }
    ],
    "name": "HookSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "LaunchPoolKeySet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "NativeWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "tick",
        "type": "int24"
      }
    ],
    "name": "PoolInitialized",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BURN_ADDRESS",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      }
    ],
    "name": "boughtInFairLaunch",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxWhitelistAllocation",
        "type": "uint256"
      },
      {
        "internalType": "bytes32[]",
        "name": "proof",
        "type": "bytes32[]"
      }
    ],
    "name": "buyExactTokens",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "paymentIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalPayment",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "maxWhitelistAllocation",
        "type": "uint256"
      },
      {
        "internalType": "bytes32[]",
        "name": "proof",
        "type": "bytes32[]"
      }
    ],
    "name": "buyWithPayment",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "paymentIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "refund",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "internalType": "uint64",
        "name": "startsAt",
        "type": "uint64"
      },
      {
        "internalType": "uint64",
        "name": "endsAt",
        "type": "uint64"
      },
      {
        "internalType": "int24",
        "name": "initialTick",
        "type": "int24"
      },
      {
        "internalType": "uint256",
        "name": "pricePerToken",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fairLaunchSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxPerWallet",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenLiquiditySupply",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "merkleRoot",
        "type": "bytes32"
      },
      {
        "internalType": "bool",
        "name": "whitelistRequired",
        "type": "bool"
      }
    ],
    "name": "configureFairLaunch",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "int24",
        "name": "initializedTick",
        "type": "int24"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "internalType": "uint64",
        "name": "startsAt",
        "type": "uint64"
      },
      {
        "internalType": "uint64",
        "name": "endsAt",
        "type": "uint64"
      },
      {
        "internalType": "int24",
        "name": "initialTick",
        "type": "int24"
      },
      {
        "internalType": "uint256",
        "name": "targetMarketCap",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenSupplyForMarketCap",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fairLaunchSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxPerWallet",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenLiquiditySupply",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "merkleRoot",
        "type": "bytes32"
      },
      {
        "internalType": "bool",
        "name": "whitelistRequired",
        "type": "bool"
      }
    ],
    "name": "createMarketCapFairLaunch",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "int24",
        "name": "initializedTick",
        "type": "int24"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "fairLaunchInfo",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint64",
            "name": "startsAt",
            "type": "uint64"
          },
          {
            "internalType": "uint64",
            "name": "endsAt",
            "type": "uint64"
          },
          {
            "internalType": "int24",
            "name": "initialTick",
            "type": "int24"
          },
          {
            "internalType": "uint256",
            "name": "pricePerToken",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "remainingSupply",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "sold",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "revenue",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "maxPerWallet",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "tokenLiquiditySupply",
            "type": "uint256"
          },
          {
            "internalType": "bytes32",
            "name": "merkleRoot",
            "type": "bytes32"
          },
          {
            "internalType": "bool",
            "name": "whitelistRequired",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "closed",
            "type": "bool"
          }
        ],
        "internalType": "struct HoodlLaunchController.FairLaunchInfo",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "finalizeFairLaunch",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "revenue",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "nativeLiquidityAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenLiquidityAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "unsoldBurnAmount",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "closedNow",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      }
    ],
    "name": "finalizeFairLaunchFromHook",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "revenue",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "nativeLiquidityAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenLiquidityAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "unsoldBurnAmount",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "closedNow",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hoodl",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "inFairLaunchWindow",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "isFairLaunchOpen",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "maxWhitelistAllocation",
        "type": "uint256"
      },
      {
        "internalType": "bytes32[]",
        "name": "proof",
        "type": "bytes32[]"
      }
    ],
    "name": "isWhitelisted",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchPoolId",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchPoolKeySet",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "targetMarketCap",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenSupplyForMarketCap",
        "type": "uint256"
      }
    ],
    "name": "marketCapToPrice",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "pricePerToken",
        "type": "uint256"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "quotePayment",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "paymentAmount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "paymentAmount",
        "type": "uint256"
      }
    ],
    "name": "quoteTokensForPayment",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "nativeAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "rescueFairLaunch",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newHook",
        "type": "address"
      }
    ],
    "name": "setHook",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "maxWhitelistAllocation",
        "type": "uint256"
      }
    ],
    "name": "whitelistLeaf",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "withdrawNative",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import { IPoolManager } from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { IUnlockCallback } from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import { IERC20Minimal } from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol";
import { FixedPoint96 } from "@uniswap/v4-core/src/libraries/FixedPoint96.sol";
import { FullMath } from "@uniswap/v4-core/src/libraries/FullMath.sol";
import { StateLibrary } from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol";
import { BalanceDelta, BalanceDeltaLibrary } from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import { Currency } from "@uniswap/v4-core/src/types/Currency.sol";
import { ModifyLiquidityParams } from "@uniswap/v4-core/src/types/PoolOperation.sol";
import { PoolId, PoolIdLibrary } from "@uniswap/v4-core/src/types/PoolId.sol";
import { PoolKey } from "@uniswap/v4-core/src/types/PoolKey.sol";

interface IHOODLLaunchToken is IERC20Minimal {
    function hoodlGame() external view returns (address);
    function teamWallet() external view returns (address);
}

interface IHOODLLaunchGame {
    function fundSheriffPot(address from, uint256 amount) external payable;
    function grossPaymentForNetBuy(address trader, uint256 netPayment)
        external
        view
        returns (uint256 grossPayment, uint256 totalTax, uint256 teamTax, uint256 sheriffTax);
    function quoteBuyTaxIncludedForGross(address trader, uint256 grossPayment)
        external
        view
        returns (uint256 totalTax, uint256 teamTax, uint256 sheriffTax, uint256 netPayment);
    function scheduleFirstStealUnlock() external returns (uint256 unlockAt);
}

interface IHOODLLaunchHook {
    function enableTrading(PoolKey calldata key) external;
}

/// @title HoodlLaunchController
/// @notice Standalone fair-launch and v4 bootstrap module for HOODL.
contract HoodlLaunchController is IUnlockCallback {
    using BalanceDeltaLibrary for BalanceDelta;
    using PoolIdLibrary for PoolKey;

    struct FairLaunchInfo {
        uint64 startsAt;
        uint64 endsAt;
        int24 initialTick;
        uint256 pricePerToken;
        uint256 remainingSupply;
        uint256 sold;
        uint256 revenue;
        uint256 maxPerWallet;
        uint256 tokenLiquiditySupply;
        bytes32 merkleRoot;
        bool whitelistRequired;
        bool closed;
    }

    address public constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
    address public immutable hoodl;
    address public immutable poolManager;
    address public owner;
    address public hook;
    bytes32 public launchPoolId;
    bool public launchPoolKeySet;
    PoolKey private launchKey;
    mapping(bytes32 poolId => FairLaunchInfo info) private fairLaunches;
    mapping(bytes32 poolId => mapping(address buyer => uint256 amount)) public boughtInFairLaunch;
    uint256 private reentrancyStatus;

    struct BootstrapData {
        PoolKey key;
        bytes32 poolId;
        uint256 nativeLiquidityAmount;
        uint256 tokenLiquidityAmount;
        int24 initialTick;
    }

    error AmountTooLarge();
    error BadFairLaunchWindow();
    error BadLiquidityRange();
    error ExceedsFairLaunchSupply();
    error ExceedsWalletCap();
    error ExceedsWhitelistAllocation();
    error FairLaunchAlreadyStarted();
    error FairLaunchClosedAlready();
    error FairLaunchStillActive();
    error FairLaunchHookLocked();
    error FairLaunchNotConfigured();
    error FairLaunchNotActive();
    error FairLaunchNotStarted(uint256 startsAt);
    error IncorrectNativePayment();
    error InitialTickOutOfRange();
    error InvalidFairLaunchAmount();
    error InvalidProof();
    error InvalidPoolHook();
    error InvalidTaxQuote();
    error OnlyNativeHoodlPool();
    error OnlyHook();
    error OnlyPoolManager();
    error OnlyOwner();
    error PoolPriceMismatch(
        uint160 actualSqrtPriceX96,
        int24 actualTick,
        uint160 expectedSqrtPriceX96,
        int24 expectedTick
    );
    error ReentrantCall();
    error TransferFailed();
    error ZeroLiquidity();
    error ZeroAddress();

    event FairLaunchBuyTaxSettled(
        bytes32 indexed poolId,
        address indexed buyer,
        uint256 taxBaseAmount,
        uint256 totalTax,
        uint256 teamTax,
        uint256 sheriffTax
    );
    event FairLaunchClosed(
        bytes32 indexed poolId,
        uint256 revenue,
        uint256 sold,
        uint256 unsoldSupply,
        uint256 tokenLiquiditySupply,
        uint256 endedAt
    );
    event FairLaunchConfigured(
        bytes32 indexed poolId,
        uint64 startsAt,
        uint64 endsAt,
        int24 initialTick,
        uint256 pricePerToken,
        uint256 fairLaunchSupply,
        uint256 maxPerWallet,
        uint256 tokenLiquiditySupply,
        bytes32 merkleRoot,
        bool whitelistRequired
    );
    event FairLaunchCloseShortfallHandled(
        bytes32 indexed poolId,
        uint256 nativeRequested,
        uint256 nativeUsed,
        uint256 tokenLiquidityRequested,
        uint256 tokenLiquidityUsed,
        uint256 unsoldRequested,
        uint256 unsoldBurned
    );
    event FairLaunchFilled(
        bytes32 indexed poolId,
        address indexed buyer,
        uint256 tokenAmount,
        uint256 paymentAmount,
        uint256 totalPayment,
        uint256 revenue,
        uint256 remainingSupply
    );
    event FairLaunchLiquidityCreated(
        bytes32 indexed poolId,
        int24 tickLower,
        int24 tickUpper,
        uint256 tokenAmount,
        bool tokenIsCurrency0,
        uint128 liquidity
    );
    event FairLaunchRescued(
        bytes32 indexed poolId, address indexed recipient, uint256 nativeAmount, uint256 tokenAmount
    );
    event HookSet(address indexed oldHook, address indexed newHook);
    event LaunchPoolKeySet(bytes32 indexed poolId);
    event NativeWithdrawn(address indexed recipient, uint256 amount);
    event PoolInitialized(bytes32 indexed poolId, uint160 sqrtPriceX96, int24 tick);
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);
    modifier onlyOwner() {
        _onlyOwner();
        _;
    }

    modifier nonReentrant() {
        if (reentrancyStatus == 1) revert ReentrantCall();
        reentrancyStatus = 1;
        _;
        reentrancyStatus = 0;
    }

    function _onlyOwner() internal view {
        if (msg.sender != owner) revert OnlyOwner();
    }

    constructor(address hoodl_, address poolManager_) {
        if (hoodl_ == address(0) || poolManager_ == address(0)) revert ZeroAddress();

        hoodl = hoodl_;
        poolManager = poolManager_;
        owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);
    }

    receive() external payable { }

    function setHook(address newHook) external onlyOwner {
        if (newHook == address(0)) revert ZeroAddress();
        _requireHookMutable();

        address oldHook = hook;
        hook = newHook;
        emit HookSet(oldHook, newHook);
    }

    function configureFairLaunch(
        PoolKey calldata key,
        uint64 startsAt,
        uint64 endsAt,
        int24 initialTick,
        uint256 pricePerToken,
        uint256 fairLaunchSupply,
        uint256 maxPerWallet,
        uint256 tokenLiquiditySupply,
        bytes32 merkleRoot,
        bool whitelistRequired
    ) external onlyOwner returns (bytes32 poolId, int24 initializedTick) {
        (poolId, initializedTick) = _configureFairLaunch(
            key,
            startsAt,
            endsAt,
            initialTick,
            pricePerToken,
            fairLaunchSupply,
            maxPerWallet,
            tokenLiquiditySupply,
            merkleRoot,
            whitelistRequired
        );
    }

    function createMarketCapFairLaunch(
        PoolKey calldata key,
        uint64 startsAt,
        uint64 endsAt,
        int24 initialTick,
        uint256 targetMarketCap,
        uint256 tokenSupplyForMarketCap,
        uint256 fairLaunchSupply,
        uint256 maxPerWallet,
        uint256 tokenLiquiditySupply,
        bytes32 merkleRoot,
        bool whitelistRequired
    ) external onlyOwner returns (bytes32 poolId, int24 initializedTick) {
        uint256 pricePerToken = marketCapToPrice(targetMarketCap, tokenSupplyForMarketCap);
        (poolId, initializedTick) = _configureFairLaunch(
            key,
            startsAt,
            endsAt,
            initialTick,
            pricePerToken,
            fairLaunchSupply,
            maxPerWallet,
            tokenLiquiditySupply,
            merkleRoot,
            whitelistRequired
        );
    }

    function _configureFairLaunch(
        PoolKey calldata key,
        uint64 startsAt,
        uint64 endsAt,
        int24 initialTick,
        uint256 pricePerToken,
        uint256 fairLaunchSupply,
        uint256 maxPerWallet,
        uint256 tokenLiquiditySupply,
        bytes32 merkleRoot,
        bool whitelistRequired
    ) internal returns (bytes32 poolId, int24 initializedTick) {
        _requireNativeHoodlPool(key);
        _requireBootstrapableTick(initialTick, key.tickSpacing);
        poolId = PoolId.unwrap(key.toId());
        if (poolId == bytes32(0)) revert FairLaunchNotConfigured();

        FairLaunchInfo storage existing = fairLaunches[poolId];
        if (existing.startsAt != 0 && block.timestamp >= existing.startsAt) {
            revert FairLaunchAlreadyStarted();
        }
        _validateFairLaunch(
            startsAt,
            endsAt,
            pricePerToken,
            fairLaunchSupply,
            maxPerWallet,
            merkleRoot,
            whitelistRequired
        );

        launchKey = key;
        launchPoolId = poolId;
        launchPoolKeySet = true;

        fairLaunches[poolId] = FairLaunchInfo({
            startsAt: startsAt,
            endsAt: endsAt,
            initialTick: initialTick,
            pricePerToken: pricePerToken,
            remainingSupply: fairLaunchSupply,
            sold: 0,
            revenue: 0,
            maxPerWallet: maxPerWallet,
            tokenLiquiditySupply: tokenLiquiditySupply,
            merkleRoot: merkleRoot,
            whitelistRequired: whitelistRequired,
            closed: false
        });

        emit FairLaunchConfigured(
            poolId,
            startsAt,
            endsAt,
            initialTick,
            pricePerToken,
            fairLaunchSupply,
            maxPerWallet,
            tokenLiquiditySupply,
            merkleRoot,
            whitelistRequired
        );

        emit LaunchPoolKeySet(poolId);
        initializedTick = _initializeOrValidatePool(key, initialTick);
    }

    function fairLaunchInfo(bytes32 poolId) external view returns (FairLaunchInfo memory) {
        return fairLaunches[poolId];
    }

    function isFairLaunchOpen(bytes32 poolId) external view returns (bool) {
        FairLaunchInfo memory info = fairLaunches[poolId];
        return _isConfigured(info) && !info.closed;
    }

    function inFairLaunchWindow(bytes32 poolId) external view returns (bool) {
        return _inFairLaunchWindow(fairLaunches[poolId]);
    }

    function buyWithPayment(
        bytes32 poolId,
        uint256 maxWhitelistAllocation,
        bytes32[] calldata proof
    ) external payable nonReentrant returns (uint256 paymentIn, uint256 tokensOut, uint256 refund) {
        if (msg.value == 0) revert InvalidFairLaunchAmount();

        IHOODLLaunchGame game = _game();
        uint256 totalPayment = msg.value;
        uint256 teamTax;
        uint256 sheriffTax;
        (uint256 totalTax, uint256 quotedTeamTax, uint256 quotedSheriffTax, uint256 netPayment) =
            game.quoteBuyTaxIncludedForGross(msg.sender, totalPayment);
        teamTax = quotedTeamTax;
        sheriffTax = quotedSheriffTax;

        tokensOut = quoteTokensForPayment(poolId, netPayment);
        FairLaunchInfo storage info = fairLaunches[poolId];
        if (tokensOut > info.remainingSupply) {
            tokensOut = info.remainingSupply;
            paymentIn = quotePayment(poolId, tokensOut);
            (totalPayment, totalTax, teamTax, sheriffTax) =
                game.grossPaymentForNetBuy(msg.sender, paymentIn);
            // Quote round-trips are not guaranteed wei-exact; absorb any excess
            // from the revenue leg instead of letting the sellout buy revert
            // on refund underflow.
            if (totalPayment > msg.value) {
                uint256 shortfall = totalPayment - msg.value;
                if (paymentIn <= shortfall) revert InvalidTaxQuote();
                paymentIn -= shortfall;
                totalPayment = msg.value;
            }
            refund = msg.value - totalPayment;
        } else {
            paymentIn = netPayment;
        }

        _fillFairLaunchBuy(
            poolId,
            msg.sender,
            paymentIn,
            tokensOut,
            totalPayment,
            totalTax,
            teamTax,
            sheriffTax,
            maxWhitelistAllocation,
            proof
        );

        if (refund != 0) {
            _sendNative(msg.sender, refund);
        }
    }

    function buyExactTokens(
        bytes32 poolId,
        uint256 tokenAmount,
        uint256 maxWhitelistAllocation,
        bytes32[] calldata proof
    ) external payable nonReentrant returns (uint256 paymentIn, uint256 totalPayment) {
        paymentIn = quotePayment(poolId, tokenAmount);
        uint256 totalTax;
        uint256 teamTax;
        uint256 sheriffTax;
        (totalPayment, totalTax, teamTax, sheriffTax) =
            _game().grossPaymentForNetBuy(msg.sender, paymentIn);
        if (msg.value != totalPayment) revert IncorrectNativePayment();

        _fillFairLaunchBuy(
            poolId,
            msg.sender,
            paymentIn,
            tokenAmount,
            totalPayment,
            totalTax,
            teamTax,
            sheriffTax,
            maxWhitelistAllocation,
            proof
        );
    }

    function finalizeFairLaunch()
        external
        nonReentrant
        returns (
            uint256 revenue,
            uint256 nativeLiquidityAmount,
            uint256 tokenLiquidityAmount,
            uint256 unsoldBurnAmount,
            bool closedNow
        )
    {
        PoolKey memory key = _storedLaunchKey();
        return _finalizeFairLaunch(key, false);
    }

    function finalizeFairLaunchFromHook(PoolKey calldata key)
        external
        nonReentrant
        returns (
            uint256 revenue,
            uint256 nativeLiquidityAmount,
            uint256 tokenLiquidityAmount,
            uint256 unsoldBurnAmount,
            bool closedNow
        )
    {
        if (msg.sender != hook) revert OnlyHook();
        return _finalizeFairLaunch(key, true);
    }

    function _finalizeFairLaunch(PoolKey memory key, bool poolManagerAlreadyUnlocked)
        internal
        returns (
            uint256 revenue,
            uint256 nativeLiquidityAmount,
            uint256 tokenLiquidityAmount,
            uint256 unsoldBurnAmount,
            bool closedNow
        )
    {
        if (hook == address(0)) revert ZeroAddress();
        _requireNativeHoodlPool(key);

        bytes32 poolId = PoolId.unwrap(key.toId());
        FairLaunchInfo storage info = fairLaunches[poolId];
        if (!_isConfigured(info)) revert FairLaunchNotConfigured();

        revenue = info.revenue;
        uint256 tokenLiquiditySupply = info.tokenLiquiditySupply;
        uint256 unsoldSupply = info.remainingSupply;
        int24 initialTick = info.initialTick;

        if (info.closed) {
            return (revenue, 0, 0, 0, false);
        }
        if (block.timestamp < info.endsAt && info.remainingSupply != 0) {
            revert FairLaunchStillActive();
        }
        _initializeOrValidatePool(key, initialTick);

        info.closed = true;
        info.endsAt = uint64(block.timestamp);
        closedNow = true;

        nativeLiquidityAmount = _min(revenue, address(this).balance);
        uint256 tokenBalance = IERC20Minimal(hoodl).balanceOf(address(this));
        tokenLiquidityAmount = _min(tokenLiquiditySupply, tokenBalance);
        unsoldBurnAmount = _min(unsoldSupply, tokenBalance - tokenLiquidityAmount);

        if (nativeLiquidityAmount != 0 || tokenLiquidityAmount != 0) {
            BootstrapData memory bootstrap = BootstrapData({
                key: key,
                poolId: poolId,
                nativeLiquidityAmount: nativeLiquidityAmount,
                tokenLiquidityAmount: tokenLiquidityAmount,
                initialTick: initialTick
            });
            if (poolManagerAlreadyUnlocked) {
                _bootstrapFairLaunchLiquidity(bootstrap);
            } else {
                IPoolManager(poolManager).unlock(abi.encode(bootstrap));
            }
        }

        if (unsoldBurnAmount != 0) {
            _safeTransfer(hoodl, BURN_ADDRESS, unsoldBurnAmount);
        }

        if (
            nativeLiquidityAmount != revenue || tokenLiquidityAmount != tokenLiquiditySupply
                || unsoldBurnAmount != unsoldSupply
        ) {
            emit FairLaunchCloseShortfallHandled(
                poolId,
                revenue,
                nativeLiquidityAmount,
                tokenLiquiditySupply,
                tokenLiquidityAmount,
                unsoldSupply,
                unsoldBurnAmount
            );
        }

        IHOODLLaunchHook(hook).enableTrading(key);
        _game().scheduleFirstStealUnlock();

        emit FairLaunchClosed(
            poolId, revenue, info.sold, unsoldSupply, tokenLiquiditySupply, block.timestamp
        );
    }

    /// @notice Owner escape hatch for any ETH held by the controller, bounded only by
    /// balance — unlike `rescueFairLaunch` it works regardless of launch state and does
    /// not touch launch accounting. Intended for unaccounted ETH (e.g. force-sent);
    /// pre-close launch revenue should be recovered with `rescueFairLaunch` so
    /// `revenue` stays consistent.
    function withdrawNative(address recipient, uint256 amount)
        external
        onlyOwner
        nonReentrant
    {
        if (recipient == address(0)) revert ZeroAddress();
        if (amount == 0 || amount > address(this).balance) revert InvalidFairLaunchAmount();

        _sendNative(recipient, amount);
        emit NativeWithdrawn(recipient, amount);
    }

    function rescueFairLaunch(
        bytes32 poolId,
        address recipient,
        uint256 nativeAmount,
        uint256 tokenAmount
    ) external onlyOwner nonReentrant {
        if (recipient == address(0)) revert ZeroAddress();
        if (nativeAmount == 0 && tokenAmount == 0) revert InvalidFairLaunchAmount();

        FairLaunchInfo storage info = fairLaunches[poolId];
        if (!_isConfigured(info)) revert FairLaunchNotConfigured();
        if (info.closed) revert FairLaunchClosedAlready();

        if (
            nativeAmount > info.revenue || nativeAmount > address(this).balance
                || tokenAmount > _rescuableTokenSupply(info)
                || tokenAmount > IERC20Minimal(hoodl).balanceOf(address(this))
        ) {
            revert InvalidFairLaunchAmount();
        }

        if (nativeAmount != 0) {
            info.revenue -= nativeAmount;
            _sendNative(recipient, nativeAmount);
        }
        if (tokenAmount != 0) {
            _reduceLaunchTokenAccounting(info, tokenAmount);
            _safeTransfer(hoodl, recipient, tokenAmount);
        }

        emit FairLaunchRescued(poolId, recipient, nativeAmount, tokenAmount);
    }

    function marketCapToPrice(uint256 targetMarketCap, uint256 tokenSupplyForMarketCap)
        public
        pure
        returns (uint256 pricePerToken)
    {
        if (targetMarketCap == 0 || tokenSupplyForMarketCap == 0) {
            revert InvalidFairLaunchAmount();
        }

        pricePerToken = FullMath.mulDiv(targetMarketCap, 1 ether, tokenSupplyForMarketCap);
        if (pricePerToken == 0) revert InvalidFairLaunchAmount();
    }

    function quotePayment(bytes32 poolId, uint256 tokenAmount)
        public
        view
        returns (uint256 paymentAmount)
    {
        FairLaunchInfo memory info = fairLaunches[poolId];
        if (!_isConfigured(info)) revert FairLaunchNotConfigured();
        if (tokenAmount == 0) revert InvalidFairLaunchAmount();

        paymentAmount = FullMath.mulDivRoundingUp(tokenAmount, info.pricePerToken, 1 ether);
    }

    function quoteTokensForPayment(bytes32 poolId, uint256 paymentAmount)
        public
        view
        returns (uint256 tokenAmount)
    {
        FairLaunchInfo memory info = fairLaunches[poolId];
        if (!_isConfigured(info)) revert FairLaunchNotConfigured();
        if (paymentAmount == 0) revert InvalidFairLaunchAmount();

        tokenAmount = FullMath.mulDiv(paymentAmount, 1 ether, info.pricePerToken);
        if (tokenAmount == 0) revert InvalidFairLaunchAmount();
    }

    function whitelistLeaf(address account, uint256 maxWhitelistAllocation)
        public
        pure
        returns (bytes32)
    {
        // forge-lint: disable-next-line(asm-keccak256)
        return keccak256(abi.encodePacked(account, maxWhitelistAllocation));
    }

    function isWhitelisted(
        bytes32 poolId,
        address account,
        uint256 maxWhitelistAllocation,
        bytes32[] calldata proof
    ) external view returns (bool) {
        FairLaunchInfo memory info = fairLaunches[poolId];
        if (!info.whitelistRequired) {
            return true;
        }

        return _verify(proof, info.merkleRoot, whitelistLeaf(account, maxWhitelistAllocation));
    }

    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert ZeroAddress();

        address oldOwner = owner;
        owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }

    function unlockCallback(bytes calldata data) external override returns (bytes memory) {
        if (msg.sender != poolManager) revert OnlyPoolManager();

        BootstrapData memory bootstrap = abi.decode(data, (BootstrapData));
        _bootstrapFairLaunchLiquidity(bootstrap);
        return "";
    }

    function _bootstrapFairLaunchLiquidity(BootstrapData memory bootstrap) internal {
        bool hoodlIsCurrency0 = Currency.unwrap(bootstrap.key.currency0) == hoodl;
        int24 tickSpacing = bootstrap.key.tickSpacing;

        if (hoodlIsCurrency0) {
            int24 revenueUpper = _floorToSpacing(bootstrap.initialTick - 1, tickSpacing);
            int24 revenueLower = revenueUpper - tickSpacing;
            _createSingleSidedPosition(
                bootstrap.key,
                bootstrap.poolId,
                revenueLower,
                revenueUpper,
                bootstrap.nativeLiquidityAmount,
                false
            );

            int24 tokenLower = _ceilToSpacing(bootstrap.initialTick + 1, tickSpacing);
            int24 tokenUpper = TickMath.maxUsableTick(tickSpacing);
            _createSingleSidedPosition(
                bootstrap.key,
                bootstrap.poolId,
                tokenLower,
                tokenUpper,
                bootstrap.tokenLiquidityAmount,
                true
            );
        } else {
            int24 revenueLower = _ceilToSpacing(bootstrap.initialTick + 1, tickSpacing);
            int24 revenueUpper = revenueLower + tickSpacing;
            _createSingleSidedPosition(
                bootstrap.key,
                bootstrap.poolId,
                revenueLower,
                revenueUpper,
                bootstrap.nativeLiquidityAmount,
                true
            );

            int24 tokenLower = TickMath.minUsableTick(tickSpacing);
            int24 tokenUpper = _floorToSpacing(bootstrap.initialTick - 1, tickSpacing);
            _createSingleSidedPosition(
                bootstrap.key,
                bootstrap.poolId,
                tokenLower,
                tokenUpper,
                bootstrap.tokenLiquidityAmount,
                false
            );
        }
    }

    function _fillFairLaunchBuy(
        bytes32 poolId,
        address buyer,
        uint256 paymentIn,
        uint256 tokensOut,
        uint256 totalPayment,
        uint256 totalTax,
        uint256 teamTax,
        uint256 sheriffTax,
        uint256 maxWhitelistAllocation,
        bytes32[] calldata proof
    ) internal {
        FairLaunchInfo storage info = fairLaunches[poolId];
        if (!_isConfigured(info) || info.closed) revert FairLaunchNotConfigured();
        _validateBuyTaxQuote(totalPayment, paymentIn, totalTax, teamTax, sheriffTax);
        if (block.timestamp < info.startsAt) revert FairLaunchNotStarted(info.startsAt);
        if (!_inFairLaunchWindow(info)) revert FairLaunchNotActive();
        if (paymentIn == 0 || tokensOut == 0 || totalPayment == 0) {
            revert InvalidFairLaunchAmount();
        }
        if (tokensOut > info.remainingSupply) revert ExceedsFairLaunchSupply();

        uint256 bought = boughtInFairLaunch[poolId][buyer] + tokensOut;
        if (bought > info.maxPerWallet) revert ExceedsWalletCap();
        if (info.whitelistRequired) {
            if (!_verify(proof, info.merkleRoot, whitelistLeaf(buyer, maxWhitelistAllocation))) {
                revert InvalidProof();
            }
            if (bought > maxWhitelistAllocation) revert ExceedsWhitelistAllocation();
        }

        boughtInFairLaunch[poolId][buyer] = bought;
        info.remainingSupply -= tokensOut;
        info.sold += tokensOut;
        info.revenue += paymentIn;

        _safeTransfer(hoodl, buyer, tokensOut);
        _distributeBuyTax(poolId, buyer, paymentIn, totalTax, teamTax, sheriffTax);

        emit FairLaunchFilled(
            poolId, buyer, tokensOut, paymentIn, totalPayment, info.revenue, info.remainingSupply
        );

        if (info.remainingSupply == 0 && poolId == launchPoolId) {
            _finalizeFairLaunch(_storedLaunchKey(), false);
        }
    }

    function _distributeBuyTax(
        bytes32 poolId,
        address buyer,
        uint256 paymentIn,
        uint256 totalTax,
        uint256 teamTax,
        uint256 sheriffTax
    ) internal {
        if (totalTax == 0) {
            return;
        }

        if (teamTax != 0) {
            _sendNative(IHOODLLaunchToken(hoodl).teamWallet(), teamTax);
        }

        if (sheriffTax != 0) {
            _game().fundSheriffPot{ value: sheriffTax }(buyer, sheriffTax);
        }

        emit FairLaunchBuyTaxSettled(poolId, buyer, paymentIn, totalTax, teamTax, sheriffTax);
    }

    function _validateBuyTaxQuote(
        uint256 totalPayment,
        uint256 paymentIn,
        uint256 totalTax,
        uint256 teamTax,
        uint256 sheriffTax
    ) internal pure {
        if (
            teamTax > totalTax || sheriffTax != totalTax - teamTax || totalTax > totalPayment
                || paymentIn != totalPayment - totalTax
        ) {
            revert InvalidTaxQuote();
        }
    }

    function _createSingleSidedPosition(
        PoolKey memory key,
        bytes32 poolId,
        int24 tickLower,
        int24 tickUpper,
        uint256 tokenAmount,
        bool tokenIsCurrency0
    ) internal {
        if (tokenAmount == 0) {
            return;
        }
        if (tickLower >= tickUpper) revert BadLiquidityRange();

        uint160 sqrtPriceLower = TickMath.getSqrtPriceAtTick(tickLower);
        uint160 sqrtPriceUpper = TickMath.getSqrtPriceAtTick(tickUpper);
        uint128 liquidity = tokenIsCurrency0
            ? _liquidityForAmount0(sqrtPriceLower, sqrtPriceUpper, tokenAmount)
            : _liquidityForAmount1(sqrtPriceLower, sqrtPriceUpper, tokenAmount);
        if (liquidity == 0) revert ZeroLiquidity();

        (BalanceDelta delta,) = IPoolManager(poolManager)
            .modifyLiquidity(
                key,
                ModifyLiquidityParams({
                    tickLower: tickLower,
                    tickUpper: tickUpper,
                    liquidityDelta: int256(uint256(liquidity)),
                    salt: bytes32(0)
                }),
                ""
            );

        _settleNegativeDelta(key.currency0, delta.amount0());
        _settleNegativeDelta(key.currency1, delta.amount1());

        emit FairLaunchLiquidityCreated(
            poolId, tickLower, tickUpper, tokenAmount, tokenIsCurrency0, liquidity
        );
    }

    function _settleNegativeDelta(Currency currency, int128 delta) internal {
        if (delta < 0) {
            _settleCurrency(currency, uint256(uint128(-delta)));
        }
    }

    function _settleCurrency(Currency currency, uint256 amount) internal {
        if (amount == 0) {
            return;
        }

        IPoolManager manager = IPoolManager(poolManager);
        if (Currency.unwrap(currency) == address(0)) {
            manager.settle{ value: amount }();
        } else {
            manager.sync(currency);
            _safeTransfer(Currency.unwrap(currency), address(manager), amount);
            manager.settle();
        }
    }

    function _game() internal view returns (IHOODLLaunchGame) {
        address game = IHOODLLaunchToken(hoodl).hoodlGame();
        if (game == address(0)) revert ZeroAddress();
        return IHOODLLaunchGame(game);
    }

    function _storedLaunchKey() internal view returns (PoolKey memory key) {
        if (!launchPoolKeySet) revert FairLaunchNotConfigured();
        key = launchKey;
    }

    function _requireHookMutable() internal view {
        if (!launchPoolKeySet) {
            return;
        }

        FairLaunchInfo memory info = fairLaunches[launchPoolId];
        if (_isConfigured(info) && !info.closed && block.timestamp >= info.startsAt) {
            revert FairLaunchHookLocked();
        }
    }

    function _requireNativeHoodlPool(PoolKey memory key) internal view {
        address configuredHook = hook;
        if (configuredHook == address(0)) revert ZeroAddress();
        if (address(key.hooks) != configuredHook) revert InvalidPoolHook();

        bool hoodlIsCurrency0 = Currency.unwrap(key.currency0) == hoodl;
        bool hoodlIsCurrency1 = Currency.unwrap(key.currency1) == hoodl;
        bool nativeIsCurrency0 = Currency.unwrap(key.currency0) == address(0);
        bool nativeIsCurrency1 = Currency.unwrap(key.currency1) == address(0);

        if (!((hoodlIsCurrency0 && nativeIsCurrency1) || (hoodlIsCurrency1 && nativeIsCurrency0))) {
            revert OnlyNativeHoodlPool();
        }
    }

    function _initializeOrValidatePool(PoolKey memory key, int24 initialTick)
        internal
        returns (int24 actualTick)
    {
        IPoolManager manager = IPoolManager(poolManager);
        uint160 expectedSqrtPriceX96 = TickMath.getSqrtPriceAtTick(initialTick);
        (uint160 actualSqrtPriceX96, int24 observedTick,,) =
            StateLibrary.getSlot0(manager, key.toId());
        actualTick = observedTick;

        if (actualSqrtPriceX96 == 0) {
            actualTick = manager.initialize(key, expectedSqrtPriceX96);
            if (actualTick != initialTick) {
                revert PoolPriceMismatch(0, actualTick, expectedSqrtPriceX96, initialTick);
            }
            emit PoolInitialized(PoolId.unwrap(key.toId()), expectedSqrtPriceX96, actualTick);
            return actualTick;
        }

        if (actualSqrtPriceX96 != expectedSqrtPriceX96 || actualTick != initialTick) {
            revert PoolPriceMismatch(
                actualSqrtPriceX96, actualTick, expectedSqrtPriceX96, initialTick
            );
        }
    }

    function _rescuableTokenSupply(FairLaunchInfo storage info) internal view returns (uint256) {
        return info.tokenLiquiditySupply + info.remainingSupply;
    }

    function _reduceLaunchTokenAccounting(FairLaunchInfo storage info, uint256 tokenAmount)
        internal
    {
        uint256 liquidityReduction = _min(tokenAmount, info.tokenLiquiditySupply);
        if (liquidityReduction != 0) {
            info.tokenLiquiditySupply -= liquidityReduction;
        }

        uint256 remainingReduction = tokenAmount - liquidityReduction;
        if (remainingReduction != 0) {
            info.remainingSupply -= remainingReduction;
        }
    }

    function _sendNative(address recipient, uint256 amount) internal {
        if (recipient == address(0)) revert ZeroAddress();
        (bool sent,) = payable(recipient).call{ value: amount }("");
        if (!sent) revert TransferFailed();
    }

    function _safeTransfer(address token, address to, uint256 amount) internal {
        bool success = IERC20Minimal(token).transfer(to, amount);
        if (!success) revert TransferFailed();
    }

    function _min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    function _liquidityForAmount0(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount0)
        internal
        pure
        returns (uint128 liquidity)
    {
        if (sqrtPriceAX96 > sqrtPriceBX96) {
            (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96);
        }

        uint256 intermediate = FullMath.mulDiv(sqrtPriceAX96, sqrtPriceBX96, FixedPoint96.Q96);
        liquidity =
            _toUint128(FullMath.mulDiv(amount0, intermediate, sqrtPriceBX96 - sqrtPriceAX96));
    }

    function _liquidityForAmount1(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount1)
        internal
        pure
        returns (uint128 liquidity)
    {
        if (sqrtPriceAX96 > sqrtPriceBX96) {
            (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96);
        }

        liquidity =
            _toUint128(FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtPriceBX96 - sqrtPriceAX96));
    }

    /// @dev Both close-time single-sided bands must fit inside the usable tick range:
    /// one full spacing-wide band below `floor(initialTick - 1)` and one above
    /// `ceil(initialTick + 1)`, whichever side each currency ordering assigns them.
    /// Validating here means `_bootstrapFairLaunchLiquidity` can never revert on
    /// out-of-range ticks and wedge the lazy-finalize path.
    function _requireBootstrapableTick(int24 initialTick, int24 tickSpacing) internal pure {
        if (
            _floorToSpacing(initialTick - 1, tickSpacing) - tickSpacing
                < TickMath.minUsableTick(tickSpacing)
                || _ceilToSpacing(initialTick + 1, tickSpacing) + tickSpacing
                    > TickMath.maxUsableTick(tickSpacing)
        ) {
            revert InitialTickOutOfRange();
        }
    }

    function _floorToSpacing(int24 tick, int24 tickSpacing) internal pure returns (int24) {
        int256 tick256 = int256(tick);
        int256 spacing256 = int256(tickSpacing);
        int256 compressed = tick256 / spacing256;
        if (tick256 < 0 && tick256 % spacing256 != 0) {
            compressed--;
        }

        return int24(compressed * spacing256);
    }

    function _ceilToSpacing(int24 tick, int24 tickSpacing) internal pure returns (int24) {
        int256 tick256 = int256(tick);
        int256 spacing256 = int256(tickSpacing);
        int256 compressed = tick256 / spacing256;
        if (tick256 > 0 && tick256 % spacing256 != 0) {
            compressed++;
        }

        return int24(compressed * spacing256);
    }

    function _toUint128(uint256 value) internal pure returns (uint128) {
        if (value > type(uint128).max) revert AmountTooLarge();
        return uint128(value);
    }

    function _validateFairLaunch(
        uint64 startsAt,
        uint64 endsAt,
        uint256 pricePerToken,
        uint256 fairLaunchSupply,
        uint256 maxPerWallet,
        bytes32 merkleRoot,
        bool whitelistRequired
    ) internal pure {
        if (startsAt >= endsAt) revert BadFairLaunchWindow();
        if (pricePerToken == 0 || fairLaunchSupply == 0 || maxPerWallet == 0) {
            revert InvalidFairLaunchAmount();
        }
        if (maxPerWallet > fairLaunchSupply) revert ExceedsFairLaunchSupply();
        if (whitelistRequired && merkleRoot == bytes32(0)) revert InvalidProof();
    }

    function _isConfigured(FairLaunchInfo memory info) internal pure returns (bool) {
        return info.startsAt != 0;
    }

    function _inFairLaunchWindow(FairLaunchInfo memory info) internal view returns (bool) {
        return block.timestamp >= info.startsAt && block.timestamp < info.endsAt
            && info.remainingSupply > 0;
    }

    function _verify(bytes32[] memory proof, bytes32 root, bytes32 leaf)
        internal
        pure
        returns (bool)
    {
        bytes32 computedHash = leaf;

        for (uint256 i = 0; i < proof.length; i++) {
            bytes32 proofElement = proof[i];
            computedHash = computedHash <= proofElement
                ? keccak256(abi.encodePacked(computedHash, proofElement))
                : keccak256(abi.encodePacked(proofElement, computedHash));
        }

        return computedHash == root;
    }
}
Chain explorer2124msChain node98ms