EquifoldV4DexAdapter

0x64aa9cbe29edd1b047988eebba7fcd3c82add693

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
12,643 bytes
ABI entries
43
10 read · 8 write
License
none

Contract information

Address
0x64aa9cbe29edd1b047988eebba7fcd3c82add693
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x9183764e3f…14b89a2427

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (10)

feeCollector()address
graduationFeeBps()uint16
graduationManager()address
hook()address
liquidityCount()uint256
liquidityLocked(uint256)bool
owner()address
pendingOwner()address
poolManager()address
treasury()address

Events (7)

FeeConfiguredFeesCollectedGraduationFeeTakenGraduationManagerConfiguredLiquidityCreatedOwnershipTransferStartedOwnershipTransferred

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "contract EquifoldV4Hook",
        "name": "hook_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "initialOwner",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyConfigured",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeCapExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeConfigNotSet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "IncorrectCaller",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidData",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidProject",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidSqrtPrice",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownLiquidity",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroLiquidity",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "graduationFeeBps",
        "type": "uint16"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "feeCollector",
        "type": "address"
      }
    ],
    "name": "FeeConfigured",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "liquidityId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "creatorQuote",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "protocolQuote",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenDonated",
        "type": "uint256"
      }
    ],
    "name": "FeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "liquidityId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "platformFee",
        "type": "uint256"
      }
    ],
    "name": "GraduationFeeTaken",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "graduationManager",
        "type": "address"
      }
    ],
    "name": "GraduationManagerConfigured",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "quoteAsset",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "liquidityId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "liquidity",
        "type": "uint128"
      }
    ],
    "name": "LiquidityCreated",
    "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"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "liquidityId",
        "type": "uint256"
      }
    ],
    "name": "collectFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "creatorQuote",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "protocolQuote",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenDonated",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "quoteAsset",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "quoteAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minTokenAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minQuoteAmount",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "createLiquidity",
    "outputs": [
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "liquidityId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeCollector",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "graduationFeeBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "graduationManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract EquifoldV4Hook",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "liquidityCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "liquidityId",
        "type": "uint256"
      }
    ],
    "name": "liquidityLocked",
    "outputs": [
      {
        "internalType": "bool",
        "name": "locked",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "graduationFeeBps_",
        "type": "uint16"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeCollector_",
        "type": "address"
      }
    ],
    "name": "setFeeConfig",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "graduationManager_",
        "type": "address"
      }
    ],
    "name": "setGraduationManager",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "rawData",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { Ownable, Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IPoolManager } from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { IUnlockCallback } from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import { IHooks } from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import { PoolKey } from "@uniswap/v4-core/src/types/PoolKey.sol";
import { PoolIdLibrary } from "@uniswap/v4-core/src/types/PoolId.sol";
import { Currency } from "@uniswap/v4-core/src/types/Currency.sol";
import { BalanceDelta, BalanceDeltaLibrary } from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import { ModifyLiquidityParams } from "@uniswap/v4-core/src/types/PoolOperation.sol";
import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol";
import { FullMath } from "@uniswap/v4-core/src/libraries/FullMath.sol";
import { LPFeeLibrary } from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import { IDexAdapter } from "../interfaces/IDexAdapter.sol";
import { IWrappedNative } from "../interfaces/IWrappedNative.sol";
import { EquifoldV4Hook } from "./EquifoldV4Hook.sol";
import { LiquidityAmounts } from "./LiquidityAmounts.sol";

/// @dev Minimal call surface for {LaunchpadFeeCollector.accrueCreatorFees}. Declared locally
///      instead of importing the concrete contract — which self-pins to exactly 0.8.24 for
///      bytecode stability — so this file's flexible ^0.8.24 pragma can still compile at 0.8.26
///      alongside the v4-core test suite (which needs an exactly-0.8.26 `PoolManager`). The two
///      pragma pins are mutually exclusive within one compilation unit; a bare interface has no
///      transitive dependency on the concrete file, so it sidesteps the conflict entirely.
interface ILaunchpadFeeAccruer {
    function accrueCreatorFees(
        uint256 projectNumber
    ) external payable;
}

/// @title Equifold Uniswap v4 Graduation Adapter
/// @author Equifold
/// @notice Turns a graduated bonding curve into a Uniswap v4 pool with a dynamic (market-cap
///         tiered) fee and PERMANENTLY LOCKED full-range liquidity. Implements {IDexAdapter} so the
///         GraduationManager can call it exactly like the mock: it pulls the released token + WETH,
///         skims an optional graduation platform fee, initializes the pool at the graduation price,
///         adds full-range liquidity through the unlock/settle flow, and keeps the position forever
///         (there is no withdrawal path). Anyone can later crank {collectFees} to route the pool's
///         accrued dynamic-fee share to the project's creator and the protocol treasury.
/// @dev Consumes EXACTLY the amounts the manager approved (it pulls the full token+quote so the
///      manager returns to its pre-release baseline); any liquidity-math dust stays locked here.
contract EquifoldV4DexAdapter is IDexAdapter, IUnlockCallback, Ownable2Step, ReentrancyGuard {
    using SafeERC20 for IERC20;
    using BalanceDeltaLibrary for BalanceDelta;
    using PoolIdLibrary for PoolKey;

    error AlreadyConfigured();
    error FeeCapExceeded();
    error FeeConfigNotSet();
    error IncorrectCaller();
    error InvalidAddress();
    error InvalidData();
    error InvalidProject();
    error InvalidSqrtPrice();
    error SlippageExceeded();
    error TransferFailed();
    error UnknownLiquidity();
    error ZeroLiquidity();

    uint256 private constant Q192 = 0x1000000000000000000000000000000000000000000000000; // 2**192
    int24 private constant TICK_SPACING = 60;
    uint16 private constant MAX_GRADUATION_FEE_BPS = 500;
    uint16 private constant MAX_BPS = 10_000;

    /// @dev Discriminates the two operations {unlockCallback} can be asked to perform.
    enum CallbackKind {
        AddLiquidity,
        Collect
    }

    struct AddLiquidityCallback {
        PoolKey key;
        int24 tickLower;
        int24 tickUpper;
        uint128 liquidity;
    }

    /// @notice Everything needed to crank fees for a graduated pool later.
    struct Position {
        PoolKey key;
        address token;
        address quoteAsset;
        uint256 projectNumber;
    }

    IPoolManager public immutable poolManager;
    EquifoldV4Hook public immutable hook;
    address public graduationManager;
    uint256 public liquidityCount;
    mapping(uint256 liquidityId => bool locked) public liquidityLocked;
    mapping(uint256 liquidityId => Position) private _positions;
    /// @dev Quote assets this adapter has ever built a pool against; gates {receive}.
    mapping(address quoteAsset => bool known) private _knownQuoteAssets;

    /// @notice Basis points of each graduation's quote amount skimmed to {treasury} (0 = disabled).
    uint16 public graduationFeeBps;
    /// @notice Protocol fee destination for both the graduation skim and the collect-fee split.
    address public treasury;
    /// @notice The {LaunchpadFeeCollector} that receives and splits each project's creator share.
    address public feeCollector;

    event GraduationManagerConfigured(address indexed graduationManager);
    event FeeConfigured(
        uint16 graduationFeeBps, address indexed treasury, address indexed feeCollector
    );
    event LiquidityCreated(
        address indexed token,
        address indexed quoteAsset,
        uint256 indexed liquidityId,
        uint160 sqrtPriceX96,
        uint128 liquidity
    );
    event GraduationFeeTaken(uint256 indexed liquidityId, uint256 platformFee);
    event FeesCollected(
        uint256 indexed liquidityId,
        uint256 indexed projectNumber,
        uint256 creatorQuote,
        uint256 protocolQuote,
        uint256 tokenDonated
    );

    constructor(
        IPoolManager poolManager_,
        EquifoldV4Hook hook_,
        address initialOwner
    ) Ownable(initialOwner) {
        if (
            address(poolManager_) == address(0) || address(hook_) == address(0)
                || initialOwner == address(0)
        ) revert InvalidAddress();
        poolManager = poolManager_;
        hook = hook_;
    }

    /// @notice One-time wiring of the GraduationManager allowed to call `createLiquidity`.
    function setGraduationManager(
        address graduationManager_
    ) external onlyOwner {
        if (graduationManager != address(0)) revert AlreadyConfigured();
        if (graduationManager_ == address(0) || graduationManager_.code.length == 0) {
            revert InvalidAddress();
        }
        graduationManager = graduationManager_;
        emit GraduationManagerConfigured(graduationManager_);
    }

    /// @notice Configures the graduation platform fee and where every {collectFees} payout is
    ///         routed (creator share via {feeCollector}, protocol remainder via {treasury}). The
    ///         creator/protocol split itself is no longer a ratio set here — it's the hook's exact
    ///         per-swap bookkeeping, see {EquifoldV4Hook.consumeCreatorOwed}. Owner-gated; may be
    ///         re-run to retune. Until this is called the graduation fee defaults to zero (fully
    ///         backward compatible with pools graduated before it is set).
    function setFeeConfig(
        uint16 graduationFeeBps_,
        address treasury_,
        address feeCollector_
    ) external onlyOwner {
        if (graduationFeeBps_ > MAX_GRADUATION_FEE_BPS) {
            revert FeeCapExceeded();
        }
        if (
            treasury_ == address(0) || feeCollector_ == address(0) || feeCollector_.code.length == 0
        ) {
            revert InvalidAddress();
        }
        graduationFeeBps = graduationFeeBps_;
        treasury = treasury_;
        feeCollector = feeCollector_;
        emit FeeConfigured(graduationFeeBps_, treasury_, feeCollector_);
    }

    /// @inheritdoc IDexAdapter
    function createLiquidity(
        address token,
        address quoteAsset,
        uint256 tokenAmount,
        uint256 quoteAmount,
        uint256 minTokenAmount,
        uint256 minQuoteAmount,
        bytes calldata data
    ) external nonReentrant returns (address pool, uint256 liquidityId) {
        uint256 projectNumber = _validateCreateLiquidity(
            token, quoteAsset, tokenAmount, quoteAmount, minTokenAmount, minQuoteAmount, data
        );

        // Pull the full released amounts so the manager returns to its pre-release baseline.
        IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount);
        IERC20(quoteAsset).safeTransferFrom(msg.sender, address(this), quoteAmount);

        liquidityId =
            _buildAndStoreLiquidity(token, quoteAsset, tokenAmount, quoteAmount, projectNumber);
        // The adapter itself is the permanent locker: it holds the position and has no exit path.
        return (address(this), liquidityId);
    }

    /// @dev Validates a `createLiquidity` call and returns the decoded project number. Split out of
    ///      `createLiquidity` to keep that function's stack shallow.
    function _validateCreateLiquidity(
        address token,
        address quoteAsset,
        uint256 tokenAmount,
        uint256 quoteAmount,
        uint256 minTokenAmount,
        uint256 minQuoteAmount,
        bytes calldata data
    ) private view returns (uint256 projectNumber) {
        if (msg.sender != graduationManager) revert IncorrectCaller();
        if (data.length != 32) revert InvalidData();
        projectNumber = abi.decode(data, (uint256));
        if (projectNumber == 0) revert InvalidProject();
        if (token == address(0) || quoteAsset == address(0)) revert InvalidAddress();
        if (tokenAmount < minTokenAmount || quoteAmount < minQuoteAmount) {
            revert SlippageExceeded();
        }
    }

    /// @dev Skims the graduation platform fee, builds the pool with the net quote amount, and
    ///      records the position. Split out of `createLiquidity` to keep that function's stack
    ///      shallow (assumes the full token+quote amounts were already pulled from the caller).
    function _buildAndStoreLiquidity(
        address token,
        address quoteAsset,
        uint256 tokenAmount,
        uint256 quoteAmount,
        uint256 projectNumber
    ) private returns (uint256 liquidityId) {
        // Graduation platform fee: skimmed from the quote side before the pool is seeded. Zero
        // (the default until {setFeeConfig} is called) skims nothing, so this is backward compatible.
        (uint256 netQuoteAmount, uint256 platformFee) = _takeGraduationFee(quoteAsset, quoteAmount);

        (uint160 sqrtPriceX96, uint128 liquidity, PoolKey memory key) =
            _createPool(token, quoteAsset, tokenAmount, netQuoteAmount);

        liquidityId = ++liquidityCount;
        liquidityLocked[liquidityId] = true;
        _knownQuoteAssets[quoteAsset] = true;
        _positions[liquidityId] = Position({
            key: key, token: token, quoteAsset: quoteAsset, projectNumber: projectNumber
        });
        if (platformFee > 0) emit GraduationFeeTaken(liquidityId, platformFee);
        emit LiquidityCreated(token, quoteAsset, liquidityId, sqrtPriceX96, liquidity);
    }

    /// @dev Transfers `graduationFeeBps` of `quoteAmount` to {treasury} (no-op while unset) and
    ///      returns the remaining net amount the pool should be seeded with.
    function _takeGraduationFee(
        address quoteAsset,
        uint256 quoteAmount
    ) private returns (uint256 netQuoteAmount, uint256 platformFee) {
        platformFee = (quoteAmount * graduationFeeBps) / MAX_BPS;
        netQuoteAmount = quoteAmount - platformFee;
        if (platformFee > 0) {
            IERC20(quoteAsset).safeTransfer(treasury, platformFee);
        }
    }

    /// @notice Permissionlessly cranks a graduated pool's accrued dynamic-fee share. Pokes the
    ///         permanently-locked position, donates the token-side amount straight back into the
    ///         pool (it never leaves the locked liquidity), and splits the quote-side amount taken
    ///         out between the project's creator (via {LaunchpadFeeCollector.accrueCreatorFees}) and
    ///         the protocol {treasury}.
    /// @dev Reverts if `liquidityId` is unknown or fee config was never set; returns zeros without
    ///      reverting when nothing has accrued since the last crank.
    function collectFees(
        uint256 liquidityId
    )
        external
        nonReentrant
        returns (uint256 creatorQuote, uint256 protocolQuote, uint256 tokenDonated)
    {
        Position memory position = _requirePosition(liquidityId);
        if (treasury == address(0) || feeCollector == address(0)) revert FeeConfigNotSet();

        bytes memory result =
            poolManager.unlock(abi.encode(CallbackKind.Collect, abi.encode(liquidityId)));
        uint256 quoteCollected;
        (quoteCollected, tokenDonated) = abi.decode(result, (uint256, uint256));

        if (quoteCollected > 0) {
            (creatorQuote, protocolQuote) = _splitAndRoute(position, quoteCollected);
        }
        emit FeesCollected(
            liquidityId, position.projectNumber, creatorQuote, protocolQuote, tokenDonated
        );
    }

    /// @dev Pulls the hook's exact per-swap creator bookkeeping for this pool (atomically zeroing
    ///      it), clamps it to what was actually collected (bookkeeping can round up by a few wei
    ///      relative to the position's own fee-growth accounting), and routes each share; unwraps
    ///      the quote asset (assumed WETH-like) to ETH for both the creator accrual and the
    ///      treasury transfer.
    function _splitAndRoute(
        Position memory position,
        uint256 quoteCollected
    ) private returns (uint256 creatorQuote, uint256 protocolQuote) {
        uint256 owed = hook.consumeCreatorOwed(position.key.toId());
        creatorQuote = owed > quoteCollected ? quoteCollected : owed;
        protocolQuote = quoteCollected - creatorQuote;
        if (creatorQuote > 0) {
            IWrappedNative(position.quoteAsset).withdraw(creatorQuote);
            ILaunchpadFeeAccruer(feeCollector).accrueCreatorFees{ value: creatorQuote }(
                position.projectNumber
            );
        }
        if (protocolQuote > 0) {
            IWrappedNative(position.quoteAsset).withdraw(protocolQuote);
            (bool success,) = payable(treasury).call{ value: protocolQuote }("");
            if (!success) revert TransferFailed();
        }
    }

    /// @dev Initializes the v4 pool at the graduation price, registers it with the hook, and adds
    ///      full-range liquidity through the unlock/settle flow. Returns the price, liquidity, and
    ///      key (the latter stored per-liquidityId for later {collectFees} cranks).
    function _createPool(
        address token,
        address quoteAsset,
        uint256 tokenAmount,
        uint256 quoteAmount
    ) private returns (uint160 sqrtPriceX96, uint128 liquidity, PoolKey memory key) {
        key = _buildKey(token, quoteAsset);
        int24 tickLower;
        int24 tickUpper;
        (sqrtPriceX96, liquidity, tickLower, tickUpper) =
            _priceAndLiquidity(token < quoteAsset, tokenAmount, quoteAmount);
        if (liquidity == 0) revert ZeroLiquidity();

        poolManager.initialize(key, sqrtPriceX96);
        hook.registerPool(key, token);
        poolManager.unlock(
            abi.encode(
                CallbackKind.AddLiquidity,
                abi.encode(
                    AddLiquidityCallback({
                        key: key, tickLower: tickLower, tickUpper: tickUpper, liquidity: liquidity
                    })
                )
            )
        );
    }

    /// @dev Builds the pool key with the canonical currency ordering (currency0 < currency1).
    function _buildKey(
        address token,
        address quoteAsset
    ) private view returns (PoolKey memory key) {
        (address currency0, address currency1) =
            token < quoteAsset ? (token, quoteAsset) : (quoteAsset, token);
        key = PoolKey({
            currency0: Currency.wrap(currency0),
            currency1: Currency.wrap(currency1),
            fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
            tickSpacing: TICK_SPACING,
            hooks: IHooks(address(hook))
        });
    }

    /// @dev The graduation-price sqrt (`sqrt(amount1/amount0) * 2^96`) and the full-range liquidity.
    function _priceAndLiquidity(
        bool tokenIsCurrency0,
        uint256 tokenAmount,
        uint256 quoteAmount
    )
        private
        pure
        returns (uint160 sqrtPriceX96, uint128 liquidity, int24 tickLower, int24 tickUpper)
    {
        (uint256 amount0, uint256 amount1) =
            tokenIsCurrency0 ? (tokenAmount, quoteAmount) : (quoteAmount, tokenAmount);
        uint256 sqrtPrice = Math.sqrt(FullMath.mulDiv(amount1, Q192, amount0));
        // Reject any price v4 itself would reject; this also makes the uint160 cast non-truncating.
        if (sqrtPrice < TickMath.MIN_SQRT_PRICE || sqrtPrice > TickMath.MAX_SQRT_PRICE) {
            revert InvalidSqrtPrice();
        }
        // forge-lint: disable-next-line(unsafe-typecast)
        sqrtPriceX96 = uint160(sqrtPrice); // bounded above by MAX_SQRT_PRICE < 2**160
        (tickLower, tickUpper) = _fullRangeTicks();
        liquidity = LiquidityAmounts.getLiquidityForAmounts(
            sqrtPriceX96,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            amount0,
            amount1
        );
    }

    /// @dev The full-range tick bounds every Equifold graduation position uses.
    function _fullRangeTicks() private pure returns (int24 tickLower, int24 tickUpper) {
        tickLower = TickMath.minUsableTick(TICK_SPACING);
        tickUpper = TickMath.maxUsableTick(TICK_SPACING);
    }

    /// @dev Reverts with {UnknownLiquidity} unless `liquidityId` has a stored position.
    function _requirePosition(
        uint256 liquidityId
    ) private view returns (Position memory position) {
        position = _positions[liquidityId];
        if (position.token == address(0)) revert UnknownLiquidity();
    }

    /// @notice v4 unlock callback: dispatches to the add-liquidity or fee-collection handler.
    function unlockCallback(
        bytes calldata rawData
    ) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert IncorrectCaller();
        (CallbackKind kind, bytes memory payload) = abi.decode(rawData, (CallbackKind, bytes));
        if (kind == CallbackKind.AddLiquidity) return _handleAddLiquidity(payload);
        return _handleCollect(payload);
    }

    /// @dev Adds the full-range liquidity for a fresh graduation and settles what the pool is owed.
    function _handleAddLiquidity(
        bytes memory payload
    ) private returns (bytes memory) {
        AddLiquidityCallback memory data = abi.decode(payload, (AddLiquidityCallback));

        // uint128 liquidity always fits int256; positive delta adds liquidity.
        // forge-lint: disable-next-line(unsafe-typecast)
        int256 liquidityDelta = int256(uint256(data.liquidity));
        (BalanceDelta delta,) = poolManager.modifyLiquidity(
            data.key,
            ModifyLiquidityParams({
                tickLower: data.tickLower,
                tickUpper: data.tickUpper,
                liquidityDelta: liquidityDelta,
                salt: bytes32(0)
            }),
            ""
        );

        _settle(data.key.currency0, delta.amount0());
        _settle(data.key.currency1, delta.amount1());
        return "";
    }

    /// @dev Pokes the permanently-locked position for accrued fees (a zero-liquidity-delta
    ///      `modifyLiquidity` call), donates the token-side amount straight back to the pool so it
    ///      neither needs taking nor settling, and takes the quote-side amount to this adapter for
    ///      {collectFees} to split. Returns `abi.encode(quoteOwed, tokenOwed)`.
    function _handleCollect(
        bytes memory payload
    ) private returns (bytes memory) {
        uint256 liquidityId = abi.decode(payload, (uint256));
        Position storage position = _positions[liquidityId];
        (int24 tickLower, int24 tickUpper) = _fullRangeTicks();

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

        bool tokenIsCurrency0 = Currency.unwrap(position.key.currency0) == position.token;
        (uint256 tokenOwed, uint256 quoteOwed) = _splitByToken(delta, tokenIsCurrency0);

        if (tokenOwed > 0) {
            (uint256 donate0, uint256 donate1) =
                tokenIsCurrency0 ? (tokenOwed, uint256(0)) : (uint256(0), tokenOwed);
            poolManager.donate(position.key, donate0, donate1, "");
        }
        if (quoteOwed > 0) {
            Currency quoteCurrency =
                tokenIsCurrency0 ? position.key.currency1 : position.key.currency0;
            poolManager.take(quoteCurrency, address(this), quoteOwed);
        }
        return abi.encode(quoteOwed, tokenOwed);
    }

    /// @dev Maps a poke's raw currency0/currency1 delta onto (tokenOwed, quoteOwed), clamped to 0.
    function _splitByToken(
        BalanceDelta delta,
        bool tokenIsCurrency0
    ) private pure returns (uint256 tokenOwed, uint256 quoteOwed) {
        uint256 amount0 = _positiveAmount(delta.amount0());
        uint256 amount1 = _positiveAmount(delta.amount1());
        (tokenOwed, quoteOwed) = tokenIsCurrency0 ? (amount0, amount1) : (amount1, amount0);
    }

    /// @dev Returns `amount` as a uint256 when positive (owed to this adapter), else 0.
    function _positiveAmount(
        int128 amount
    ) private pure returns (uint256) {
        if (amount <= 0) return 0;
        // forge-lint: disable-next-line(unsafe-typecast)
        return uint256(uint128(amount));
    }

    /// @dev Pays the pool manager any currency this adapter owes (negative delta).
    function _settle(
        Currency currency,
        int128 amount
    ) private {
        if (amount >= 0) return; // nothing owed on this side
        // amount < 0 here, so -amount is a positive int128 that fits uint128.
        // forge-lint: disable-next-line(unsafe-typecast)
        uint256 owed = uint256(uint128(-amount));
        poolManager.sync(currency);
        IERC20(Currency.unwrap(currency)).safeTransfer(address(poolManager), owed);
        poolManager.settle();
    }

    /// @notice Accepts ETH only when unwrapping a known quote asset during {collectFees}.
    receive() external payable {
        if (!_knownQuoteAssets[msg.sender]) revert IncorrectCaller();
    }
}
Chain explorer1829msChain node83ms