CashCatLiquidityLocker

0x8c4b82d15a82f5f56e04a927de5a8c861f9a9d0e

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
9,167 bytes
ABI entries
62
20 read · 11 write
License
none

Contract information

Address
0x8c4b82d15a82f5f56e04a927de5a8c861f9a9d0e
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x91f840401a…4fb5478010

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (20)

BPS_DENOMINATOR()uint256
MAX_PROTOCOL_FEE_BPS()uint256
deployerTokenCount(address)uint256
deployerTokensAt(address, uint256, uint256)address[]
feeRecipientTokenCount(address)uint256
feeRecipientTokensAt(address, uint256, uint256)address[]
feeRedirects(address)address
launchpad()address
lockedTokens(address)bool
onERC721Received(address, address, uint256, bytes)bytes4
owner()address
pendingFees(address, address)uint256
pendingOwner()address
poolManager()address
positionManager()address
positionToken(uint256)address
protocolFeeBps()uint256
protocolFeeRecipient()address
renounceOwnership()
tokenProtocolFeeBps(address)uint256

Events (11)

FeeRedirectUpdatedFeesCollectedLaunchpadSetOwnershipTransferStartedOwnershipTransferredPayoutDeferredPendingFeesReassignedPendingFeesWithdrawnPositionLockedProtocolFeeBpsUpdatedProtocolFeeRecipientUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "initialOwner",
        "type": "address"
      },
      {
        "internalType": "contract IPositionManager",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "initialProtocolFeeRecipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "initialProtocolFeeBps",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "InvalidFeeRecipient",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidProtocolFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LaunchpadAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NativeTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoFeesToCollect",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotAPositionNft",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotAuthorized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotLaunchpad",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NothingToWithdraw",
    "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": "OwnershipCannotBeRenounced",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PositionAlreadyLocked",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PositionNotHeld",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenNotFound",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newFeeWallet",
        "type": "address"
      }
    ],
    "name": "FeeRedirectUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "caller",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "creatorRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount0",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount1",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "protocolAmount0",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "protocolAmount1",
        "type": "uint256"
      }
    ],
    "name": "FeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "launchpad",
        "type": "address"
      }
    ],
    "name": "LaunchpadSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "currency",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "PayoutDeferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "currency",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "PendingFeesReassigned",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "currency",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "PendingFeesWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "deployer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "protocolFeeBps",
        "type": "uint256"
      }
    ],
    "name": "PositionLocked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "bps",
        "type": "uint256"
      }
    ],
    "name": "ProtocolFeeBpsUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "ProtocolFeeRecipientUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS_DENOMINATOR",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_PROTOCOL_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "collectFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount1",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "deployer",
        "type": "address"
      }
    ],
    "name": "deployerTokenCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "deployer",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "start",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "deployerTokensAt",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "page",
        "type": "address[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "escrowLaunchFee",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "feeRecipientTokenCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "start",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "feeRecipientTokensAt",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "page",
        "type": "address[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "feeRedirects",
    "outputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchpad",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "lockPosition",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "lockedTokens",
    "outputs": [
      {
        "internalType": "bool",
        "name": "locked",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "name": "onERC721Received",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "currency",
        "type": "address"
      }
    ],
    "name": "pendingFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "contract IPositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      }
    ],
    "name": "positionToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolFeeBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolFeeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "currency",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "reassignPending",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "newFeeWallet",
        "type": "address"
      }
    ],
    "name": "setFeeRedirect",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "launchpad_",
        "type": "address"
      }
    ],
    "name": "setLaunchpad",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "bps",
        "type": "uint256"
      }
    ],
    "name": "setProtocolFeeBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "setProtocolFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "tokenProtocolFeeBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "bps",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "currency",
        "type": "address"
      }
    ],
    "name": "withdrawPending",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

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

import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";

import {ICashCatLaunchpad, ICashCatLiquidityLocker} from "./interfaces/ICashCat.sol";

/**
 * @title CashCatLiquidityLocker
 * @notice Permanently holds the Uniswap v4 position NFTs of every CashCat
 * launch and streams the accrued swap fees to the token creator and the
 * protocol.
 *
 * The permanence guarantee is structural: this contract exposes no function
 * that decreases position liquidity, burns a position, transfers a position
 * NFT or performs an arbitrary external call. Fee collection uses a
 * zero-liquidity decrease, which by construction can only realize accrued
 * fees, never principal.
 *
 * Fee payouts push funds to the recipients; when a push fails (reverting
 * receiver, token-level block) the amount is escrowed in `pendingFees` and can
 * be pulled later with {withdrawPending}, so one unreachable recipient can
 * never block the other side's revenue.
 */
contract CashCatLiquidityLocker is Ownable2Step, ReentrancyGuard, IERC721Receiver, ICashCatLiquidityLocker {
    using SafeERC20 for IERC20;

    uint256 public constant BPS_DENOMINATOR = 10_000;
    /// @notice Hard cap on the protocol's share of LP fees (50%).
    uint256 public constant MAX_PROTOCOL_FEE_BPS = 5000;

    /// @notice Gas forwarded to an untrusted native recipient on a fee push.
    /// Enough for an EOA or a plain receive()/multisig, capped so a gas-burning
    /// recipient cannot deny the co-recipient's payout in the same collection;
    /// a recipient needing more pulls via withdrawPending instead.
    uint256 private constant NATIVE_PUSH_GAS = 30_000;

    error NotLaunchpad();
    error NotAuthorized();
    error LaunchpadAlreadySet();
    error TokenNotFound();
    error PositionNotHeld();
    error PositionAlreadyLocked();
    error NoFeesToCollect();
    error InvalidProtocolFee();
    error InvalidFeeRecipient();
    error NothingToWithdraw();
    error NativeTransferFailed();
    error NotAPositionNft();
    error OwnershipCannotBeRenounced();
    error ZeroAddress();

    event LaunchpadSet(address indexed launchpad);
    event PositionLocked(
        address indexed token, address indexed deployer, uint256 indexed positionId, uint256 protocolFeeBps
    );
    event FeesCollected(
        address indexed token,
        address indexed caller,
        address creatorRecipient,
        uint256 amount0,
        uint256 amount1,
        uint256 protocolAmount0,
        uint256 protocolAmount1
    );
    event FeeRedirectUpdated(address indexed token, address indexed newFeeWallet);
    event ProtocolFeeRecipientUpdated(address indexed recipient);
    event ProtocolFeeBpsUpdated(uint256 bps);
    event PayoutDeferred(address indexed recipient, address indexed currency, uint256 amount);
    event PendingFeesWithdrawn(address indexed recipient, address indexed currency, uint256 amount);
    event PendingFeesReassigned(
        address indexed from, address indexed to, address indexed currency, uint256 amount
    );

    /// @notice Uniswap v4 PositionManager holding the locked position NFTs.
    IPositionManager public immutable positionManager;
    /// @notice Uniswap v4 PoolManager; the only address allowed to send native ETH here.
    address public immutable poolManager;

    /// @notice The launchpad bound to this locker (one-shot set by the owner).
    address public launchpad;

    /// @notice Recipient of the protocol's fee share and of launch fees.
    address public override protocolFeeRecipient;
    /// @notice Protocol share (in bps) snapshotted by each future launch.
    uint256 public protocolFeeBps;

    /// @notice Protocol fee share fixed for a given token at launch time.
    mapping(address token => uint256 bps) public tokenProtocolFeeBps;
    /// @notice Tokens whose position is registered and permanently locked here.
    mapping(address token => bool locked) public lockedTokens;
    /// @notice Reverse index enforcing one launch position ↔ one token, so the
    /// locker validates the exact NFT it was handed rather than merely that it
    /// owns *some* position (which is always true).
    mapping(uint256 positionId => address token) public positionToken;
    /// @notice Optional creator-fee redirect per token (defaults to the deployer).
    mapping(address token => address recipient) public feeRedirects;
    /// @notice Escrowed payouts that could not be pushed; `currency == address(0)` is native ETH.
    mapping(address recipient => mapping(address currency => uint256 amount)) public pendingFees;

    mapping(address deployer => address[] tokens) private _deployerTokens;
    mapping(address recipient => address[] tokens) private _feeRecipientTokens;
    mapping(address token => uint256 indexPlusOne) private _feeRecipientTokenIndex;

    constructor(
        address initialOwner,
        IPositionManager positionManager_,
        address poolManager_,
        address initialProtocolFeeRecipient,
        uint256 initialProtocolFeeBps
    ) Ownable(initialOwner) {
        if (address(positionManager_) == address(0) || poolManager_ == address(0)) {
            revert ZeroAddress();
        }
        if (initialProtocolFeeRecipient == address(0)) revert ZeroAddress();
        if (initialProtocolFeeBps > MAX_PROTOCOL_FEE_BPS) revert InvalidProtocolFee();

        positionManager = positionManager_;
        poolManager = poolManager_;
        protocolFeeRecipient = initialProtocolFeeRecipient;
        protocolFeeBps = initialProtocolFeeBps;
    }

    modifier onlyLaunchpad() {
        if (msg.sender != launchpad) revert NotLaunchpad();
        _;
    }

    /// @dev Native fee proceeds arrive from the PoolManager during collection;
    /// all other senders are rejected so ETH cannot get stranded here.
    receive() external payable {
        if (msg.sender != poolManager) revert NotAuthorized();
    }

    /**
     * @notice ERC-721 receiver hook. Rejects every incoming transfer.
     * @dev The v4 PositionManager mints launch positions with a plain `_mint`
     * (verified: no receiver callback), so this hook is NEVER invoked by a
     * CashCat launch — the position is minted with the locker already set as
     * owner. There is no registration path for a position arriving any other
     * way: `lockPosition` is `onlyLaunchpad` and cross-checks the launch record,
     * and the locker exposes no NFT-transfer or arbitrary-call surface, so an
     * accepted stray position would be stranded forever. Reverting makes an
     * accidental `safeTransferFrom` fail loudly instead of silently eating the NFT.
     */
    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
        revert NotAPositionNft();
    }

    /// @notice Disabled. The protocol fee stream, recipient, and per-token fee
    /// bookkeeping are all owner-administered; renouncing would strand fee
    /// revenue in escrow with no way to re-point it. Ownership hand-off still
    /// uses the 2-step flow.
    function renounceOwnership() public view override onlyOwner {
        revert OwnershipCannotBeRenounced();
    }

    /**
     * @notice Escrows a launch fee that the launchpad could not push directly
     * to the protocol recipient, crediting it for pull withdrawal.
     * @dev Lets the launchpad keep launches live even if `protocolFeeRecipient`
     * is temporarily unable to receive ETH, instead of reverting every launch.
     */
    function escrowLaunchFee(address to) external payable override onlyLaunchpad {
        if (to == address(0)) revert ZeroAddress();
        pendingFees[to][address(0)] += msg.value;
        emit PayoutDeferred(to, address(0), msg.value);
    }

    /**
     * @notice Binds the locker to one launchpad. One-shot.
     */
    function setLaunchpad(address launchpad_) external onlyOwner {
        if (launchpad != address(0)) revert LaunchpadAlreadySet();
        if (launchpad_ == address(0)) revert ZeroAddress();
        // Verify the reverse binding before committing this one-shot pointer,
        // matching the launchpad's own setTokenDeployer check so a mis-wired
        // deployment reverts instead of locking in permanently.
        if (address(ICashCatLaunchpad(launchpad_).locker()) != address(this)) revert NotAuthorized();
        launchpad = launchpad_;
        emit LaunchpadSet(launchpad_);
    }

    /**
     * @notice Registers permanent custody of a freshly minted launch position.
     * @dev Called by the launchpad in the launch transaction after minting the
     * position NFT directly to this contract.
     */
    function lockPosition(address token) external override onlyLaunchpad {
        if (lockedTokens[token]) revert PositionAlreadyLocked();

        ICashCatLaunchpad.LaunchedToken memory launched = ICashCatLaunchpad(launchpad).getLaunchedToken(token);
        if (!launched.exists || launched.token != token) revert TokenNotFound();
        if (IERC721(address(positionManager)).ownerOf(launched.positionId) != address(this)) {
            revert PositionNotHeld();
        }
        // Enforce one-position ↔ one-token from the locker's own state. The
        // ownerOf check only proves the locker owns SOME position (always true);
        // this proves the id has not already been claimed by another token.
        if (positionToken[launched.positionId] != address(0)) revert PositionAlreadyLocked();

        lockedTokens[token] = true;
        positionToken[launched.positionId] = token;
        uint256 snapshotBps = protocolFeeBps;
        tokenProtocolFeeBps[token] = snapshotBps;
        _deployerTokens[launched.deployer].push(token);

        emit PositionLocked(token, launched.deployer, launched.positionId, snapshotBps);
    }

    /**
     * @notice Realizes accrued swap fees for a launch token and distributes
     * them between the protocol and the creator (or their redirect).
     * @dev Callable by anyone: recipients are fixed by configuration, so open
     * collection is keeper-friendly and grants callers nothing.
     */
    function collectFees(address token) external nonReentrant returns (uint256 amount0, uint256 amount1) {
        ICashCatLaunchpad.LaunchedToken memory launched = ICashCatLaunchpad(launchpad).getLaunchedToken(token);
        if (!launched.exists || !lockedTokens[token]) revert TokenNotFound();

        (amount0, amount1) = _realizeFees(launched);
        if (amount0 == 0 && amount1 == 0) revert NoFeesToCollect();

        address creator = feeRedirects[token];
        if (creator == address(0)) creator = launched.deployer;

        uint256 shareBps = tokenProtocolFeeBps[token];
        uint256 protocolAmount0 = (amount0 * shareBps) / BPS_DENOMINATOR;
        uint256 protocolAmount1 = (amount1 * shareBps) / BPS_DENOMINATOR;

        address currency0 = Currency.unwrap(launched.poolKey.currency0);
        address currency1 = Currency.unwrap(launched.poolKey.currency1);
        _pay(currency0, protocolFeeRecipient, protocolAmount0);
        _pay(currency1, protocolFeeRecipient, protocolAmount1);
        _pay(currency0, creator, amount0 - protocolAmount0);
        _pay(currency1, creator, amount1 - protocolAmount1);

        emit FeesCollected(token, msg.sender, creator, amount0, amount1, protocolAmount0, protocolAmount1);
    }

    /**
     * @dev Realizes accrued fees for a locked position: a zero-liquidity
     * decrease (which by construction cannot touch principal) followed by
     * TAKE_PAIR to this contract. Fee amounts cannot be sandwiched, so the
     * decrease minimums are 0. Returns amounts measured by balance change.
     */
    function _realizeFees(ICashCatLaunchpad.LaunchedToken memory launched)
        private
        returns (uint256 amount0, uint256 amount1)
    {
        address currency0 = Currency.unwrap(launched.poolKey.currency0);
        address currency1 = Currency.unwrap(launched.poolKey.currency1);
        uint256 balance0Before = _selfBalance(currency0);
        uint256 balance1Before = _selfBalance(currency1);

        bytes memory actions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR));
        bytes[] memory params = new bytes[](2);
        params[0] = abi.encode(launched.positionId, uint256(0), uint128(0), uint128(0), bytes(""));
        params[1] = abi.encode(launched.poolKey.currency0, launched.poolKey.currency1, address(this));
        positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp);

        amount0 = _selfBalance(currency0) - balance0Before;
        amount1 = _selfBalance(currency1) - balance1Before;
    }

    /**
     * @notice Withdraws payouts that were escrowed after a failed push.
     * @param currency Token address, or address(0) for native ETH.
     */
    function withdrawPending(address currency) external nonReentrant {
        uint256 amount = pendingFees[msg.sender][currency];
        if (amount == 0) revert NothingToWithdraw();
        pendingFees[msg.sender][currency] = 0;

        if (currency == address(0)) {
            (bool ok,) = payable(msg.sender).call{value: amount}("");
            if (!ok) revert NativeTransferFailed();
        } else {
            IERC20(currency).safeTransfer(msg.sender, amount);
        }

        emit PendingFeesWithdrawn(msg.sender, currency, amount);
    }

    /**
     * @notice Reassigns the caller's escrowed balance in `currency` to another
     * address, giving the escrow a terminal recovery path.
     * @dev Escapes the case where the credited address cannot execute
     * {withdrawPending} (a contract with no payable fallback, or a recipient the
     * token later blocklisted): a blocklisted or fallback-less party can still
     * sign this call and forward its balance to an address that can withdraw.
     * Pure internal accounting — no external call, no NFT transfer, no liquidity
     * change — so every structural guarantee of the locker is preserved.
     */
    function reassignPending(address currency, address newRecipient) external nonReentrant {
        // Reject in-system sinks that cannot withdraw, matching the fee setters —
        // reassigning there would strand the caller's own escrow. (Self-grief only,
        // but the guard removes the footgun and keeps the invariant consistent.)
        if (
            newRecipient == address(0) || newRecipient == address(this) || newRecipient == launchpad
                || newRecipient == address(positionManager) || newRecipient == poolManager
        ) revert InvalidFeeRecipient();
        uint256 amount = pendingFees[msg.sender][currency];
        if (amount == 0) revert NothingToWithdraw();
        pendingFees[msg.sender][currency] = 0;
        pendingFees[newRecipient][currency] += amount;
        emit PendingFeesReassigned(msg.sender, newRecipient, currency, amount);
    }

    /**
     * @notice Redirects the creator fee share for one token.
     * @dev Callable by the token's deployer, or by the launchpad during launch.
     */
    function setFeeRedirect(address token, address newFeeWallet) external override {
        ICashCatLaunchpad.LaunchedToken memory launched = ICashCatLaunchpad(launchpad).getLaunchedToken(token);
        if (!launched.exists) revert TokenNotFound();
        if (msg.sender != launched.deployer && msg.sender != launchpad) revert NotAuthorized();
        _setFeeRedirect(token, newFeeWallet);
    }

    /**
     * @notice Changes the protocol fee recipient.
     */
    function setProtocolFeeRecipient(address recipient) external onlyOwner {
        // Reject in-system addresses that silently strand fees: the launchpad
        // has no withdrawPending caller and no ERC20 sweep, and the locker's own
        // receive() rejects the launchpad's push. address(0) is rejected too.
        if (
            recipient == address(0) || recipient == address(this) || recipient == launchpad
                || recipient == address(positionManager) || recipient == poolManager
        ) revert InvalidFeeRecipient();
        protocolFeeRecipient = recipient;
        emit ProtocolFeeRecipientUpdated(recipient);
    }

    /**
     * @notice Changes the fee share snapshotted by future launches. Live
     * tokens keep the share fixed at their launch.
     */
    function setProtocolFeeBps(uint256 bps) external onlyOwner {
        if (bps > MAX_PROTOCOL_FEE_BPS) revert InvalidProtocolFee();
        protocolFeeBps = bps;
        emit ProtocolFeeBpsUpdated(bps);
    }

    /**
     * @notice Number of tokens launched by a deployer.
     */
    function deployerTokenCount(address deployer) external view returns (uint256) {
        return _deployerTokens[deployer].length;
    }

    /**
     * @notice Page of tokens launched by a deployer.
     */
    function deployerTokensAt(address deployer, uint256 start, uint256 count)
        external
        view
        returns (address[] memory page)
    {
        return _page(_deployerTokens[deployer], start, count);
    }

    /**
     * @notice Number of tokens whose creator fees are redirected to a recipient.
     */
    function feeRecipientTokenCount(address recipient) external view returns (uint256) {
        return _feeRecipientTokens[recipient].length;
    }

    /**
     * @notice Page of tokens whose creator fees are redirected to a recipient.
     * @dev Membership is attacker-influenceable (anyone can redirect their own
     * token's fees to any wallet), so it MUST NOT be treated as an authorization
     * or association signal. For the authentic set of a wallet's launches, read
     * `deployerTokensAt`, which is written only from the recorded deployer.
     */
    function feeRecipientTokensAt(address recipient, uint256 start, uint256 count)
        external
        view
        returns (address[] memory page)
    {
        return _page(_feeRecipientTokens[recipient], start, count);
    }

    /**
     * @dev Stores a creator-fee redirect and keeps the per-recipient token
     * index consistent (swap-and-pop removal from the previous recipient).
     */
    function _setFeeRedirect(address token, address newFeeWallet) private {
        // address(0) means "default to the deployer"; any other in-system
        // address would silently strand the creator's fee share, so reject it.
        if (
            newFeeWallet == address(this) || newFeeWallet == launchpad
                || newFeeWallet == address(positionManager) || newFeeWallet == poolManager
        ) revert InvalidFeeRecipient();

        address previousFeeWallet = feeRedirects[token];
        if (previousFeeWallet == newFeeWallet) return;

        uint256 previousIndexPlusOne = _feeRecipientTokenIndex[token];
        if (previousFeeWallet != address(0) && previousIndexPlusOne != 0) {
            address[] storage previousTokens = _feeRecipientTokens[previousFeeWallet];
            uint256 previousIndex = previousIndexPlusOne - 1;
            uint256 lastIndex = previousTokens.length - 1;
            if (previousIndex != lastIndex) {
                address movedToken = previousTokens[lastIndex];
                previousTokens[previousIndex] = movedToken;
                _feeRecipientTokenIndex[movedToken] = previousIndex + 1;
            }
            previousTokens.pop();
            delete _feeRecipientTokenIndex[token];
        }

        feeRedirects[token] = newFeeWallet;
        if (newFeeWallet != address(0)) {
            _feeRecipientTokens[newFeeWallet].push(token);
            _feeRecipientTokenIndex[token] = _feeRecipientTokens[newFeeWallet].length;
        }

        emit FeeRedirectUpdated(token, newFeeWallet);
    }

    /**
     * @dev Pushes a payout; on failure escrows it for {withdrawPending} so a
     * broken recipient can never block collection for everyone else.
     */
    function _pay(address currency, address to, uint256 amount) private {
        if (amount == 0) return;

        bool ok;
        if (currency == address(0)) {
            // Cap gas so a gas-burning recipient cannot exhaust the transaction
            // and deny the co-recipient's leg; a recipient needing more pulls it
            // via withdrawPending, which forwards all gas.
            (ok,) = payable(to).call{value: amount, gas: NATIVE_PUSH_GAS}("");
        } else {
            ok = IERC20(currency).trySafeTransfer(to, amount);
        }

        if (!ok) {
            pendingFees[to][currency] += amount;
            emit PayoutDeferred(to, currency, amount);
        }
    }

    function _selfBalance(address currency) private view returns (uint256) {
        return currency == address(0) ? address(this).balance : IERC20(currency).balanceOf(address(this));
    }

    function _page(address[] storage list, uint256 start, uint256 count) private view returns (address[] memory page) {
        uint256 length = list.length;
        if (start >= length) return new address[](0);
        // Overflow-safe clamp: length - start cannot underflow after the check above.
        uint256 end = count > length - start ? length : start + count;
        page = new address[](end - start);
        for (uint256 i = start; i < end; ++i) {
            page[i - start] = list[i];
        }
    }
}
Chain explorer1658msChain node95ms