SteakHouseV4Factory

0x97bec1a8b3ed7caac59f687a51f898b75b774e83

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
21,612 bytes
ABI entries
89
32 read · 5 write
License
none

Contract information

Address
0x97bec1a8b3ed7caac59f687a51f898b75b774e83
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x3235e0cbe1…000cf3eeb8

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (32)

BPS()uint256
MAX_LP_FEE()uint24
MIN_LP_FEE()uint24
MODE_BAND()uint8
MODE_TIME()uint8
PERMIT2()address
SNIPER_MAX_BPS()uint24
SNIPER_MIN_BPS()uint24
STARTING_MCAP()uint256
TICK_LOWER()int24
TICK_UPPER()int24
TIME_MAX_FEE_START()uint24
TIME_MAX_INTERVAL()uint256
TIME_MIN_INTERVAL()uint256
TOTAL_SUPPLY()uint256
UNBRANDING_FEE()uint256
VANITY_FEE()uint256
antiPvpRegistry()address
config()address
failedFeeETH(bytes32)uint256
hook()address
launchByPoolId(bytes32)tuple
launchByToken(address)tuple
owner()address
poolActivated(bytes32)bool
poolManager()address
poolStates(bytes32)address, address, uint256, uint256, bool
positionManager()address
positionTokenId(bytes32)uint256
previewSqrtPrice(int24)uint160
tokenIdToPoolId(uint256)bytes32
totalLaunches()uint256

Events (12)

FailedFeesRecoveredFeeSendFailedFeeSplitDistributedFeesCollectedFromPositionLaunchFeePaidLiquidityAddedLiquidityRepositionedOwnershipTransferredPoolActivatedPositionMintedPositionRetiredTokenLaunched

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract SteakHouseV4Config",
        "name": "_config",
        "type": "address"
      },
      {
        "internalType": "contract IPoolManager",
        "name": "_poolManager",
        "type": "address"
      },
      {
        "internalType": "contract SteakHouseV4Hook",
        "name": "_hook",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_positionManager",
        "type": "address"
      },
      {
        "internalType": "contract AntiPVPRegistry",
        "name": "_antiPvpRegistry",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_owner",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AgentOnly",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyMinted",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyRetired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AntiPVPDuplicateCombo",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EmptyName",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EmptySymbol",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FirstBuyWindowActive",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientBootstrap",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientFees",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidFeeSplit",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidParams",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidPosition",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidSalt",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidSniperTax",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidTargetMcap",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidTimeConfig",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidTokenId",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LaunchNotFound",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LiquidityAddFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NameTooLong",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotAuthorized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotMinted",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyCreatorOrOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyHook",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyPoolManager",
    "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": "PoolIdNotFound",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ProtocolPaused",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SqrtPriceOverflow",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SymbolTooLong",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenApprovalFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddressInSplit",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroBeneficiary",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroLiquidity",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FailedFeesRecovered",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeeSendFailed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "wallet",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeeSplitDistributed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "positionIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "FeesCollectedFromPosition",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "LaunchFeePaid",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "liquidityMinted",
        "type": "uint256"
      }
    ],
    "name": "LiquidityAdded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "tickLower",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "tickUpper",
        "type": "int24"
      }
    ],
    "name": "LiquidityRepositioned",
    "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": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "isDevActivation",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "liquidityMinted",
        "type": "uint256"
      }
    ],
    "name": "PoolActivated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "positionIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "PositionMinted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "positionIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethRecovered",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensRecovered",
        "type": "uint256"
      }
    ],
    "name": "PositionRetired",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "beneficiary",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      }
    ],
    "name": "TokenLaunched",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_LP_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_LP_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MODE_BAND",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MODE_TIME",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PERMIT2",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SNIPER_MAX_BPS",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SNIPER_MIN_BPS",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "STARTING_MCAP",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_LOWER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_UPPER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TIME_MAX_FEE_START",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TIME_MAX_INTERVAL",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TIME_MIN_INTERVAL",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "UNBRANDING_FEE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "VANITY_FEE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "antiPvpRegistry",
    "outputs": [
      {
        "internalType": "contract AntiPVPRegistry",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "positionIndex",
        "type": "uint256"
      }
    ],
    "name": "collectFeesFromPosition",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "config",
    "outputs": [
      {
        "internalType": "contract SteakHouseV4Config",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "address",
            "name": "beneficiary",
            "type": "address"
          },
          {
            "components": [
              {
                "internalType": "address[5]",
                "name": "wallets",
                "type": "address[5]"
              },
              {
                "internalType": "uint16[5]",
                "name": "percentages",
                "type": "uint16[5]"
              },
              {
                "internalType": "uint8",
                "name": "count",
                "type": "uint8"
              }
            ],
            "internalType": "struct SteakHouseV4Factory.FeeSplit",
            "name": "feeSplit",
            "type": "tuple"
          },
          {
            "internalType": "uint24[8]",
            "name": "lpFeesPerBand",
            "type": "uint24[8]"
          },
          {
            "internalType": "uint256[8]",
            "name": "maxWalletBps",
            "type": "uint256[8]"
          },
          {
            "internalType": "uint256[8]",
            "name": "maxTxBps",
            "type": "uint256[8]"
          },
          {
            "internalType": "bool",
            "name": "branded",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "vanity",
            "type": "bool"
          },
          {
            "internalType": "bytes32",
            "name": "customSalt",
            "type": "bytes32"
          },
          {
            "internalType": "uint8",
            "name": "mode",
            "type": "uint8"
          },
          {
            "components": [
              {
                "internalType": "uint64",
                "name": "maxTxInterval",
                "type": "uint64"
              },
              {
                "internalType": "uint64",
                "name": "maxWalletInterval",
                "type": "uint64"
              },
              {
                "internalType": "uint64",
                "name": "feeDropInterval",
                "type": "uint64"
              },
              {
                "internalType": "uint24",
                "name": "feeStart",
                "type": "uint24"
              },
              {
                "internalType": "uint24",
                "name": "feeFloor",
                "type": "uint24"
              },
              {
                "internalType": "uint256",
                "name": "maxTxStart",
                "type": "uint256"
              },
              {
                "internalType": "uint256",
                "name": "maxTxStep",
                "type": "uint256"
              },
              {
                "internalType": "uint256",
                "name": "maxWalletStart",
                "type": "uint256"
              },
              {
                "internalType": "uint256",
                "name": "maxWalletStep",
                "type": "uint256"
              },
              {
                "internalType": "uint256",
                "name": "feeDropStep",
                "type": "uint256"
              }
            ],
            "internalType": "struct TimeLimits.TimeConfig",
            "name": "timeConfig",
            "type": "tuple"
          },
          {
            "internalType": "bool",
            "name": "sniperTaxEnabled",
            "type": "bool"
          },
          {
            "internalType": "uint24",
            "name": "sniperTaxBps",
            "type": "uint24"
          }
        ],
        "internalType": "struct SteakHouseV4Factory.CreateParams",
        "name": "params",
        "type": "tuple"
      }
    ],
    "name": "createLaunch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "PoolId",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "failedFeeETH",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract SteakHouseV4Hook",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "launchByPoolId",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "token",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "beneficiary",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "PoolId",
            "name": "poolId",
            "type": "bytes32"
          },
          {
            "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": "poolKey",
            "type": "tuple"
          },
          {
            "internalType": "uint256",
            "name": "createdAt",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "createdBlock",
            "type": "uint256"
          },
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "components": [
              {
                "internalType": "address[5]",
                "name": "wallets",
                "type": "address[5]"
              },
              {
                "internalType": "uint16[5]",
                "name": "percentages",
                "type": "uint16[5]"
              },
              {
                "internalType": "uint8",
                "name": "count",
                "type": "uint8"
              }
            ],
            "internalType": "struct SteakHouseV4Factory.FeeSplit",
            "name": "feeSplit",
            "type": "tuple"
          },
          {
            "internalType": "uint24[8]",
            "name": "lpFeesPerBand",
            "type": "uint24[8]"
          },
          {
            "internalType": "uint256[8]",
            "name": "maxWalletBps",
            "type": "uint256[8]"
          },
          {
            "internalType": "uint256[8]",
            "name": "maxTxBps",
            "type": "uint256[8]"
          },
          {
            "internalType": "bool",
            "name": "branded",
            "type": "bool"
          }
        ],
        "internalType": "struct SteakHouseV4Factory.LaunchInfo",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "launchByToken",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "token",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "beneficiary",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "PoolId",
            "name": "poolId",
            "type": "bytes32"
          },
          {
            "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": "poolKey",
            "type": "tuple"
          },
          {
            "internalType": "uint256",
            "name": "createdAt",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "createdBlock",
            "type": "uint256"
          },
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "components": [
              {
                "internalType": "address[5]",
                "name": "wallets",
                "type": "address[5]"
              },
              {
                "internalType": "uint16[5]",
                "name": "percentages",
                "type": "uint16[5]"
              },
              {
                "internalType": "uint8",
                "name": "count",
                "type": "uint8"
              }
            ],
            "internalType": "struct SteakHouseV4Factory.FeeSplit",
            "name": "feeSplit",
            "type": "tuple"
          },
          {
            "internalType": "uint24[8]",
            "name": "lpFeesPerBand",
            "type": "uint24[8]"
          },
          {
            "internalType": "uint256[8]",
            "name": "maxWalletBps",
            "type": "uint256[8]"
          },
          {
            "internalType": "uint256[8]",
            "name": "maxTxBps",
            "type": "uint256[8]"
          },
          {
            "internalType": "bool",
            "name": "branded",
            "type": "bool"
          }
        ],
        "internalType": "struct SteakHouseV4Factory.LaunchInfo",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "poolActivated",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "PoolId",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "poolStates",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "beneficiary",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "totalSupply",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "recycledETH",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "activated",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "positionTokenId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      }
    ],
    "name": "previewSqrtPrice",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "recoverFailedFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "tokenIdToPoolId",
    "outputs": [
      {
        "internalType": "PoolId",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalLaunches",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "v4-periphery/src/libraries/Actions.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
import {LiquidityAmounts} from "v4-periphery/src/libraries/LiquidityAmounts.sol";
import {PositionInfo, PositionInfoLibrary} from "v4-periphery/src/libraries/PositionInfoLibrary.sol";

import {SteakHouseV4Token} from "./SteakHouseV4Token.sol";
import {Token} from "./SteakHouseV4TokenSteakHouse.sol";
import {SteakHouseV4Config} from "./SteakHouseV4Config.sol";
import {SteakHouseV4Hook} from "./SteakHouseV4Hook.sol";
import {AntiPVPRegistry} from "./AntiPVPRegistry.sol";
import {PriceMath} from "../libraries/PriceMath.sol";
import {TimeLimits} from "../libraries/TimeLimits.sol";

contract SteakHouseV4Factory is Ownable, ReentrancyGuard {
    using PoolIdLibrary for PoolKey;
    using StateLibrary for IPoolManager;
    using PositionInfoLibrary for PositionInfo;

    uint256 public constant TOTAL_SUPPLY = 1_000_000_000 * 1e18;
    uint256 public constant BPS = 10000;
    uint256 public constant UNBRANDING_FEE = 0.001 ether;
    uint256 public constant VANITY_FEE = 0.0005 ether;

    /// @notice V4 fee unit: 1 = 0.0001%. LP fee per band must be 1.2%..10%
    ///         (12000..100000) — DIRECT V4 parity.
    uint24 public constant MIN_LP_FEE = 12000;
    uint24 public constant MAX_LP_FEE = 100000;

    /// @notice Launch limit/fee modes.
    uint8 public constant MODE_BAND = 0;
    uint8 public constant MODE_TIME = 1;
    /// @notice TIME-mode bounds (V2 parity + V4 fee floor). Fee in v4 units;
    ///         start/step limits in TOKENS validated as 0.1%..5% of supply.
    uint24 public constant TIME_MAX_FEE_START = 200000; // 20%
    uint256 public constant TIME_MIN_INTERVAL = 1;      // seconds
    uint256 public constant TIME_MAX_INTERVAL = 60;     // seconds

    /// @notice Anti-snipe tax bounds (v4 fee units, 1 = 0.0001%). When enabled the
    ///         dev picks a sniper tax in [40%, 80%] charged as the LP fee during
    ///         the first SNIPER_SECONDS after launch (standalone — it REPLACES the
    ///         normal band/time fee in that window, then normal fees resume).
    uint24 public constant SNIPER_MIN_BPS = 400000; // 40%
    uint24 public constant SNIPER_MAX_BPS = 800000; // 80%

    address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
    uint256 public constant STARTING_MCAP = 1 ether;
    int24 public constant TICK_LOWER = -887220;
    int24 public constant TICK_UPPER = 887220;

    SteakHouseV4Config public immutable config;
    IPoolManager public immutable poolManager;
    SteakHouseV4Hook public immutable hook;
    address public immutable positionManager;
    AntiPVPRegistry public immutable antiPvpRegistry;

    uint256 public totalLaunches;
    mapping(address => LaunchInfo) internal _launchByToken;
    mapping(PoolId => LaunchInfo) internal _launchByPoolId;
    mapping(PoolId => PoolState) public poolStates;
    mapping(uint256 => PoolId) public tokenIdToPoolId;

    /// @notice ETH fee shares whose push to a recipient reverted, parked per pool
    ///         so a single reverting/blacklisting fee recipient cannot brick a
    ///         pool's entire fee collection (A1 fix). Owner-recoverable.
    mapping(PoolId => uint256) public failedFeeETH;

    struct PoolState {
        address token;
        address beneficiary;
        uint256 totalSupply;
        uint256[8] positionTokenIds;
        bool[8] positionMinted;
        uint256 recycledETH;
        bool activated;
    }

    struct FeeSplit {
        address[5] wallets;
        uint16[5] percentages;
        uint8 count;
    }

    struct LaunchInfo {
        address token;
        address beneficiary;
        address creator;
        PoolId poolId;
        PoolKey poolKey;
        uint256 createdAt;
        uint256 createdBlock;
        string name;
        string symbol;
        FeeSplit feeSplit;
        uint24[8] lpFeesPerBand;
        uint256[8] maxWalletBps;
        uint256[8] maxTxBps;
        bool branded;
    }

    struct CreateParams {
        string name;
        string symbol;
        address beneficiary;
        FeeSplit feeSplit;
        uint24[8] lpFeesPerBand;
        uint256[8] maxWalletBps;
        uint256[8] maxTxBps;
        bool branded;
        bool vanity;
        bytes32 customSalt;
        uint8 mode;                       // 0 = BAND (per-band), 1 = TIME (time-based)
        TimeLimits.TimeConfig timeConfig; // only used when mode == TIME
        bool sniperTaxEnabled;            // toggle a first-window anti-snipe tax
        uint24 sniperTaxBps;              // sniper tax in v4 fee units, 40%..80% when enabled
    }

    event TokenLaunched(
        address indexed token,
        address indexed beneficiary,
        address indexed creator,
        PoolId poolId,
        uint160 sqrtPriceX96,
        string name,
        string symbol
    );

    event LaunchFeePaid(address indexed user, uint256 amount);
    event LiquidityAdded(address indexed token, PoolId indexed poolId, uint256 tokenAmount, uint256 liquidityMinted);
    event LiquidityRepositioned(address indexed token, PoolId indexed poolId, uint256 oldTokenId, uint256 newTokenId, int24 tickLower, int24 tickUpper);
    event PoolActivated(PoolId indexed poolId, bool isDevActivation, uint256 liquidityMinted);
    event PositionMinted(PoolId indexed poolId, uint256 indexed positionIndex, uint256 tokenId, uint256 tokenAmount);
    event PositionRetired(PoolId indexed poolId, uint256 indexed positionIndex, uint256 tokenId, uint256 ethRecovered, uint256 tokensRecovered);
    event FeesCollectedFromPosition(PoolId indexed poolId, uint256 indexed positionIndex, uint256 tokenId, uint256 ethAmount, uint256 tokenAmount);
    event FeeSplitDistributed(PoolId indexed poolId, address indexed wallet, uint256 amount);
    /// @notice Emitted when an ETH fee payout to a recipient fails; the amount is
    ///         parked in failedFeeETH[poolId] instead of reverting the whole
    ///         collection, so one bad recipient cannot brick fee collection.
    event FeeSendFailed(PoolId indexed poolId, address indexed recipient, uint256 amount);
    /// @notice Emitted when the owner recovers parked failed-fee ETH.
    event FailedFeesRecovered(PoolId indexed poolId, address indexed to, uint256 amount);

    error InvalidTimeConfig();
    error InvalidSniperTax();
    error InsufficientBootstrap();
    error InsufficientFees();
    error InvalidParams();
    error ProtocolPaused();
    error NameTooLong();
    error SymbolTooLong();
    error EmptyName();
    error EmptySymbol();
    error ZeroBeneficiary();
    error FeeTransferFailed();
    error InvalidTargetMcap();
    error SqrtPriceOverflow();
    error TokenApprovalFailed();
    error LiquidityAddFailed();
    error InvalidFeeSplit();
    error ZeroAddressInSplit();
    error OnlyPoolManager();
    error InvalidPosition();
    error InvalidSalt();
    error AlreadyMinted();
    error ZeroLiquidity();
    error OnlyHook();
    error NotMinted();
    error AlreadyRetired();
    error InvalidTokenId();
    error PoolIdNotFound();
    error LaunchNotFound();
    error AgentOnly();
    error OnlyCreatorOrOwner();
    error FirstBuyWindowActive();
    error NotAuthorized();
    error TransferFailed();
    error AntiPVPDuplicateCombo();

    constructor(
        SteakHouseV4Config _config,
        IPoolManager _poolManager,
        SteakHouseV4Hook _hook,
        address _positionManager,
        AntiPVPRegistry _antiPvpRegistry,
        address _owner
    ) Ownable(_owner) {
        config = _config;
        poolManager = _poolManager;
        hook = _hook;
        positionManager = _positionManager;
        antiPvpRegistry = _antiPvpRegistry;
    }

    receive() external payable {
        if (msg.sender != address(poolManager)) revert OnlyPoolManager();
    }

    /// @notice Validate a TIME-mode config (V2 parity + V4 fee floor). maxTx /
    ///         maxWallet START values are TOKEN amounts, bounded 0.1%..5% of
    ///         supply; intervals 1..60s; fee start <= 20% and floor in
    ///         [MIN_LP_FEE, feeStart].
    function _validateTimeConfig(TimeLimits.TimeConfig calldata t) internal pure {
        uint256 minStart = TOTAL_SUPPLY / 1000;        // 0.1%
        uint256 maxStart = (TOTAL_SUPPLY * 5) / 100;   // 5%
        if (t.maxTxStart < minStart || t.maxTxStart > maxStart) revert InvalidTimeConfig();
        if (t.maxWalletStart < minStart || t.maxWalletStart > maxStart) revert InvalidTimeConfig();
        if (t.maxTxInterval < TIME_MIN_INTERVAL || t.maxTxInterval > TIME_MAX_INTERVAL) revert InvalidTimeConfig();
        if (t.maxWalletInterval < TIME_MIN_INTERVAL || t.maxWalletInterval > TIME_MAX_INTERVAL) revert InvalidTimeConfig();
        if (t.feeDropInterval < TIME_MIN_INTERVAL || t.feeDropInterval > TIME_MAX_INTERVAL) revert InvalidTimeConfig();
        if (t.feeStart > TIME_MAX_FEE_START) revert InvalidTimeConfig();
        if (t.feeFloor < MIN_LP_FEE || t.feeFloor > t.feeStart) revert InvalidTimeConfig();
    }

    function createLaunch(CreateParams calldata params)
        external
        payable
        nonReentrant
        returns (address token, PoolId poolId)
    {
        if (config.paused()) revert ProtocolPaused();

        if (bytes(params.name).length > 32) revert NameTooLong();
        if (bytes(params.name).length == 0) revert EmptyName();
        if (bytes(params.symbol).length > 10) revert SymbolTooLong();
        if (bytes(params.symbol).length == 0) revert EmptySymbol();

        if (!antiPvpRegistry.isComboAvailable(params.name, params.symbol)) {
            revert AntiPVPDuplicateCombo();
        }

        if (params.beneficiary == address(0)) revert ZeroBeneficiary();

        // Calculate total fees: unbranding (0.0005) + vanity (0.0005)
        uint256 totalFees = 0;
        if (!params.branded) totalFees += UNBRANDING_FEE;
        if (params.vanity) totalFees += VANITY_FEE;

        // Carve out fees and validate bootstrap amount
        uint256 ethForBootstrap = msg.value;
        if (totalFees > 0) {
            if (msg.value < totalFees) revert InsufficientFees();
            ethForBootstrap = msg.value - totalFees;
            (bool ok,) = config.treasury().call{value: totalFees}("");
            if (!ok) revert TransferFailed();
        }

        uint256 bootstrapAmount = config.MIN_BOOTSTRAP_ETH();
        if (ethForBootstrap < bootstrapAmount) revert InsufficientBootstrap();
        bootstrapAmount = ethForBootstrap;

        if (params.mode == MODE_TIME) {
            // TIME mode: validate the time-based curve config (bands unused).
            _validateTimeConfig(params.timeConfig);
        } else {
            // BAND mode: validate per-band configuration. LP fee floor is the flat
            // MIN_LP_FEE (1.2%); the CEILING is per-band from the PriceMath table
            // (band0 20%, band1 15%, band2 10%, bands3-7 5%) so lower-MCAP bands
            // may charge more and higher bands are capped tighter.
            for (uint8 i = 0; i < 8; i++) {
                if (params.lpFeesPerBand[i] < MIN_LP_FEE) revert InvalidParams();
                if (params.lpFeesPerBand[i] > PriceMath.getMaxLpFeeForBand(i)) revert InvalidParams();
                if (params.maxWalletBps[i] < PriceMath.getMinLimitForBand(i) ||
                    params.maxWalletBps[i] > PriceMath.getMaxLimitForBand(i)) revert InvalidParams();
                if (params.maxTxBps[i] < PriceMath.getMinLimitForBand(i) ||
                    params.maxTxBps[i] > PriceMath.getMaxLimitForBand(i)) revert InvalidParams();
            }
        }

        if (params.feeSplit.count > 0) {
            if (params.feeSplit.count > 5) revert InvalidFeeSplit();

            uint256 totalPercentage = 0;
            for (uint8 i = 0; i < params.feeSplit.count; i++) {
                if (params.feeSplit.wallets[i] == address(0)) revert ZeroAddressInSplit();
                totalPercentage += params.feeSplit.percentages[i];
            }

            if (totalPercentage != BPS) revert InvalidFeeSplit();
        }

        // Anti-snipe tax: if enabled, the dev-chosen tax must be within [40%, 80%].
        // When disabled it is ignored (no sniper window).
        if (params.sniperTaxEnabled) {
            if (params.sniperTaxBps < SNIPER_MIN_BPS || params.sniperTaxBps > SNIPER_MAX_BPS) {
                revert InvalidSniperTax();
            }
        }

        // Deploy token (branded/unbranded) with a chain-bound CREATE2 salt.
        token = _deployToken(params);

        PoolKey memory key = _createPoolKey(token);

        uint256 totalSupply = TOTAL_SUPPLY;
        uint160 sqrtPriceX96 = PriceMath.calculateSqrtPrice(totalSupply, key.tickSpacing);
        poolId = key.toId();

        poolManager.initialize(key, sqrtPriceX96);

        hook.registerLaunch(key, token, params.beneficiary,
            params.lpFeesPerBand, params.maxWalletBps, params.maxTxBps,
            params.mode, params.timeConfig,
            params.sniperTaxEnabled, params.sniperTaxBps);

        (int24[8] memory tickLowers, int24[8] memory tickUppers, uint256[8] memory allocations) =
            PriceMath.calculateBandRanges(totalSupply, key.tickSpacing);

        uint256[8] memory posTokenIds;
        bool[8] memory mintedFlags;

        _tryMintBand(key, 0, tickLowers, tickUppers, allocations, sqrtPriceX96, bootstrapAmount, posTokenIds, mintedFlags);
        for (uint256 i = 1; i < 8; i++) {
            _tryMintBand(key, i, tickLowers, tickUppers, allocations, sqrtPriceX96, 0, posTokenIds, mintedFlags);
        }

        PoolState memory poolState = PoolState({
            token: token,
            beneficiary: params.beneficiary,
            totalSupply: totalSupply,
            positionTokenIds: posTokenIds,
            positionMinted: mintedFlags,
            recycledETH: 0,
            activated: true
        });
        poolStates[poolId] = poolState;

        for (uint256 i = 0; i < 8; i++) {
            if (mintedFlags[i]) {
                tokenIdToPoolId[posTokenIds[i]] = poolId;
            }
        }

        LaunchInfo memory info = LaunchInfo({
            token: token,
            beneficiary: params.beneficiary,
            creator: msg.sender,
            poolId: poolId,
            poolKey: key,
            createdAt: block.timestamp,
            createdBlock: block.number,
            name: params.name,
            symbol: params.symbol,
            feeSplit: params.feeSplit,
            lpFeesPerBand: params.lpFeesPerBand,
            maxWalletBps: params.maxWalletBps,
            maxTxBps: params.maxTxBps,
            branded: params.branded
        });

        _launchByToken[token] = info;
        _launchByPoolId[poolId] = info;
        totalLaunches++;

        emit LiquidityAdded(token, poolId, allocations[0], posTokenIds[0]);

        emit TokenLaunched(
            token,
            params.beneficiary,
            msg.sender,
            poolId,
            sqrtPriceX96,
            params.name,
            params.symbol
        );

        antiPvpRegistry.registerCombo(params.name, params.symbol);

        return (token, poolId);
    }


    function _tryMintBand(
        PoolKey memory key,
        uint256 bandIndex,
        int24[8] memory tickLowers,
        int24[8] memory tickUppers,
        uint256[8] memory allocations,
        uint160 sqrtPriceX96,
        uint256 ethForPosition,
        uint256[8] memory posTokenIds,
        bool[8] memory mintedFlags
    ) internal {
        uint128 liq = LiquidityAmounts.getLiquidityForAmounts(
            sqrtPriceX96,
            TickMath.getSqrtPriceAtTick(tickLowers[bandIndex]),
            TickMath.getSqrtPriceAtTick(tickUppers[bandIndex]),
            ethForPosition,
            allocations[bandIndex]
        );
        if (liq > 0) {
            posTokenIds[bandIndex] = _mintPositionViaManager(
                key,
                tickLowers[bandIndex],
                tickUppers[bandIndex],
                allocations[bandIndex],
                ethForPosition
            );
            mintedFlags[bandIndex] = true;
        }
    }

    function _mintPositionViaManager(
        PoolKey memory key,
        int24 tickLower,
        int24 tickUpper,
        uint256 tokenAmount,
        uint256 ethAmount
    ) internal returns (uint256 tokenId) {
        address token = Currency.unwrap(key.currency1);

        uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts(
            _getPoolPrice(key),
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            ethAmount,
            tokenAmount
        );

        if (liquidity == 0) revert ZeroLiquidity();

        SteakHouseV4Token(token).approve(PERMIT2, tokenAmount);
        IAllowanceTransfer(PERMIT2).approve(token, positionManager, type(uint160).max, type(uint48).max);

        bytes[] memory params = new bytes[](3);
        params[0] = abi.encode(key, tickLower, tickUpper, uint256(liquidity), uint128(ethAmount), uint128(tokenAmount), address(this), bytes(""));
        params[1] = abi.encode(key.currency0);
        params[2] = abi.encode(key.currency1);

        bytes memory unlockData = abi.encode(
            abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.CLOSE_CURRENCY), uint8(Actions.CLOSE_CURRENCY)),
            params
        );
        IPositionManager(positionManager).modifyLiquidities{value: ethAmount}(unlockData, block.timestamp + 1 hours);

        return IPositionManager(positionManager).nextTokenId() - 1;
    }

    function _getPoolPrice(PoolKey memory key) internal view returns (uint160) {
        (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(key.toId());
        return sqrtPriceX96;
    }

    function _withdrawPositionViaManager(
        uint256 tokenId
    ) internal returns (uint256 ethAmount, uint256 tokenAmount) {
        uint128 liquidity = IPositionManager(positionManager).getPositionLiquidity(tokenId);
        if (liquidity == 0) revert ZeroLiquidity();

        PoolId poolId = tokenIdToPoolId[tokenId];
        if (PoolId.unwrap(poolId) == bytes32(0)) revert PoolIdNotFound();

        LaunchInfo storage launch = _launchByPoolId[poolId];
        PoolKey memory key = launch.poolKey;

        uint256 ethBefore = address(this).balance;
        address token = Currency.unwrap(key.currency1);
        uint256 tokenBefore = SteakHouseV4Token(token).balanceOf(address(this));

        bytes memory actions = abi.encodePacked(
            uint8(Actions.DECREASE_LIQUIDITY),
            uint8(Actions.BURN_POSITION),
            uint8(Actions.CLOSE_CURRENCY),
            uint8(Actions.CLOSE_CURRENCY)
        );

        bytes[] memory params = new bytes[](4);
        params[0] = abi.encode(tokenId, uint256(liquidity), uint128(0), uint128(0), bytes(""));
        params[1] = abi.encode(tokenId, uint128(0), uint128(0), bytes(""));
        params[2] = abi.encode(key.currency0);
        params[3] = abi.encode(key.currency1);

        bytes memory unlockData = abi.encode(actions, params);
        uint256 deadline = block.timestamp + 1 hours;

        IPositionManager(positionManager).modifyLiquidities(unlockData, deadline);

        ethAmount = address(this).balance - ethBefore;
        tokenAmount = SteakHouseV4Token(token).balanceOf(address(this)) - tokenBefore;

        return (ethAmount, tokenAmount);
    }

    function _distributeFees(
        uint256 amount,
        bool isEth,
        address token,
        LaunchInfo storage info,
        PoolId poolId
    ) private {
        if (amount == 0) return;
        // Platform share is owner-configurable in config (0..50%); remainder to beneficiary.
        uint256 platformShare = (amount * config.platformShareBps()) / BPS;
        uint256 beneficiaryShare = amount - platformShare;

        if (platformShare > 0) {
            if (isEth) {
                _payEthOrPark(poolId, config.treasury(), platformShare);
            } else {
                SteakHouseV4Token(token).transfer(config.treasury(), platformShare);
            }
        }

        if (beneficiaryShare > 0) {
            if (info.feeSplit.count > 0) {
                for (uint8 i = 0; i < info.feeSplit.count; i++) {
                    address wallet = info.feeSplit.wallets[i];
                    uint256 walletShare = (beneficiaryShare * info.feeSplit.percentages[i]) / BPS;
                    if (walletShare > 0) {
                        if (isEth) {
                            if (_payEthOrPark(poolId, wallet, walletShare)) {
                                emit FeeSplitDistributed(poolId, wallet, walletShare);
                            }
                        } else {
                            SteakHouseV4Token(token).transfer(wallet, walletShare);
                        }
                    }
                }
            } else {
                if (isEth) {
                    _payEthOrPark(poolId, info.beneficiary, beneficiaryShare);
                } else {
                    SteakHouseV4Token(token).transfer(info.beneficiary, beneficiaryShare);
                }
            }
        }
    }

    /// @notice Send `amount` ETH to `to`; if the send reverts (recipient rejects
    ///         ETH or runs out of gas), park it in failedFeeETH[poolId] and
    ///         continue instead of reverting the whole collection. A recipient
    ///         can therefore only ever cause ITS OWN share to be parked — never
    ///         steal another recipient's share or block collection. Returns true
    ///         on a successful send. Capped gas stipend prevents a griefer from
    ///         burning collection gas.
    function _payEthOrPark(PoolId poolId, address to, uint256 amount) private returns (bool) {
        (bool ok,) = to.call{value: amount, gas: 60_000}("");
        if (!ok) {
            failedFeeETH[poolId] += amount;
            emit FeeSendFailed(poolId, to, amount);
        }
        return ok;
    }

    /// @notice Owner recovers ETH parked by _payEthOrPark for a pool (e.g. after
    ///         the offending recipient is fixed, or to forward it manually).
    ///         Sends to `to`; reverts if that send fails so nothing is lost.
    function recoverFailedFees(PoolId poolId, address to) external onlyOwner nonReentrant {
        uint256 amount = failedFeeETH[poolId];
        if (amount == 0) revert InvalidParams();
        if (to == address(0)) revert ZeroBeneficiary();
        failedFeeETH[poolId] = 0;
        (bool ok,) = to.call{value: amount}("");
        if (!ok) revert TransferFailed();
        emit FailedFeesRecovered(poolId, to, amount);
    }


    function launchByToken(address token) external view returns (LaunchInfo memory) {
        return _launchByToken[token];
    }

    function launchByPoolId(PoolId poolId) external view returns (LaunchInfo memory) {
        return _launchByPoolId[poolId];
    }

    function poolActivated(PoolId poolId) external view returns (bool) {
        return poolStates[poolId].activated;
    }

    function positionTokenId(PoolId poolId) external view returns (uint256) {
        return poolStates[poolId].positionTokenIds[0];
    }

    function previewSqrtPrice(int24 tickSpacing) external pure returns (uint160) {
        return PriceMath.calculateSqrtPrice(TOTAL_SUPPLY, tickSpacing);
    }

    function _deployToken(CreateParams calldata params) internal returns (address token) {
        bytes memory code = params.branded
            ? type(SteakHouseV4Token).creationCode
            : type(Token).creationCode;
        bytes memory args = abi.encode(params.name, params.symbol, address(this), params.beneficiary);
        bytes32 salt = _tokenSalt(
            params.vanity ? params.customSalt : bytes32(0),
            msg.sender,
            params.name,
            params.symbol
        );

        return _deployWithCreate2(code, args, salt);
    }

    function _tokenSalt(
        bytes32 userSalt,
        address creator,
        string calldata name,
        string calldata symbol
    ) internal view returns (bytes32) {
        return keccak256(abi.encode(
            "STEAKHOUSE_TOKEN_V1",
            block.chainid,
            address(this),
            creator,
            totalLaunches,
            name,
            symbol,
            userSalt
        ));
    }

    /**
     * @notice Deploy contract using CREATE2 with the effective token salt
     * @param creationCode Contract bytecode
     * @param constructorArgs Encoded constructor arguments
     * @param salt Effective salt for CREATE2
     * @return deployed Deployed contract address
     */
    function _deployWithCreate2(
        bytes memory creationCode,
        bytes memory constructorArgs,
        bytes32 salt
    ) internal returns (address deployed) {
        bytes memory bytecode = abi.encodePacked(creationCode, constructorArgs);

        assembly {
            deployed := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
        }

        if (deployed == address(0)) revert InvalidSalt();
        return deployed;
    }

    function _createPoolKey(address token) internal view returns (PoolKey memory) {
        Currency currency0 = Currency.wrap(address(0));
        Currency currency1 = Currency.wrap(token);

        return PoolKey({
            currency0: currency0,
            currency1: currency1,
            fee: 0x800000,
            tickSpacing: 60,
            hooks: IHooks(address(hook))
        });
    }

    function collectFeesFromPosition(PoolId poolId, uint256 positionIndex) external nonReentrant {
        PoolState storage state = poolStates[poolId];
        if (positionIndex >= 8) revert InvalidPosition();
        if (!state.positionMinted[positionIndex]) revert NotMinted();

        uint256 tokenId = state.positionTokenIds[positionIndex];
        if (tokenId == 0) revert InvalidTokenId();

        LaunchInfo storage info = _launchByPoolId[poolId];
        PoolKey memory key = info.poolKey;

        // Permissionless (DIRECT V4 parity): payouts only ever go to the fixed
        // treasury/feeSplit/beneficiary recipients, so anyone — including a
        // ProofOfSteak staking contract registered as a feeSplit wallet — may
        // trigger collection.

        uint256 ethBefore = address(this).balance;
        uint256 tokenBefore = SteakHouseV4Token(info.token).balanceOf(address(this));

        bytes memory actions = abi.encodePacked(
            uint8(Actions.DECREASE_LIQUIDITY),
            uint8(Actions.CLOSE_CURRENCY),
            uint8(Actions.CLOSE_CURRENCY)
        );

        bytes[] memory params = new bytes[](3);
        params[0] = abi.encode(tokenId, uint256(0), uint128(0), uint128(0), bytes(""));
        params[1] = abi.encode(key.currency0);
        params[2] = abi.encode(key.currency1);

        bytes memory unlockData = abi.encode(actions, params);
        uint256 deadline = block.timestamp + 1 hours;

        IPositionManager(positionManager).modifyLiquidities(unlockData, deadline);

        uint256 ethGained = address(this).balance - ethBefore;
        uint256 tokenGained = SteakHouseV4Token(info.token).balanceOf(address(this)) - tokenBefore;

        _distributeFees(ethGained, true, info.token, info, poolId);
        _distributeFees(tokenGained, false, info.token, info, poolId);

        emit FeesCollectedFromPosition(poolId, positionIndex, tokenId, ethGained, tokenGained);
    }
}
Chain explorer3050msChain node85ms