EquifoldV3LaunchModule

0xfda86c98297811ea09273f6b80d8547a290872c6

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
9,691 bytes
ABI entries
37
19 read · 3 write
License
none

Contract information

Address
0xfda86c98297811ea09273f6b80d8547a290872c6
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x0194bc7aa6…6b32ca2205

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (19)

creator()address
feeCollector()address
initialTick()int24
isGraduated()bool
isToken0()bool
moduleFactory()address
pairedPrincipal()uint256
pool()address
poolFee()uint24
positionId()uint256
positionManager()address
progressToGraduation()uint16
projectNumber()uint256
standardTokenFactory()address
swapRouter()address
token()address
treasury()address
v3Factory()address
weth()address

Events (2)

FeesCollectedTokenInitialized

ABI

[
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "moduleFactory",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "standardTokenFactory",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "address payable",
            "name": "treasury",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "feeCollector",
            "type": "address"
          },
          {
            "internalType": "contract IWrappedNative",
            "name": "weth",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "v3Factory",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "positionManager",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "swapRouter",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "projectNumber",
            "type": "uint256"
          },
          {
            "internalType": "uint24",
            "name": "poolFee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "initialTick",
            "type": "int24"
          }
        ],
        "internalType": "struct EquifoldV3LaunchModule.Initialization",
        "name": "init",
        "type": "tuple"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DeadlineExpired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoFeesToCollect",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotStandardTokenFactory",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolNotAtLaunchPrice",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnexpectedEther",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "quoteAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "FeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      }
    ],
    "name": "TokenInitialized",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "collectFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "quoteOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creator",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeCollector",
    "outputs": [
      {
        "internalType": "contract ILaunchpadTradingFeeCollector",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "initialBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "initialTick",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token_",
        "type": "address"
      }
    ],
    "name": "initializeToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "isGraduated",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "isToken0",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "moduleFactory",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pairedPrincipal",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "principal",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pool",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolFee",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "progressToGraduation",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "bps",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "projectNumber",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "standardTokenFactory",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "swapRouter",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "token",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address payable",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "v3Factory",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "contract IWrappedNative",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "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 { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol";
import { IStandardTokenLaunchModule } from "../interfaces/IStandardTokenLaunchModule.sol";
import { ILaunchpadTradingFeeCollector } from "../interfaces/ILaunchpadTradingFeeCollector.sol";
import { IWrappedNative } from "../interfaces/IWrappedNative.sol";
import {
    IUniswapV3PoolMinimal,
    INonfungiblePositionManagerMinimal,
    ISwapRouter02Minimal
} from "../interfaces/IUniswapV3Minimal.sol";
import { EquifoldRestrictedToken } from "../core/EquifoldRestrictedToken.sol";
import { LiquidityAmounts } from "./LiquidityAmounts.sol";

/// @dev Narrow callback so the module can tell its factory the real pool address once the pool is
///      created. The token/pool do not exist when the module itself is created, so the factory
///      cannot emit its DEX-discovery event at creation time; it emits it from here instead.
interface IEquifoldV3ModuleFactoryCallback {
    function notifyPoolInitialized(
        address token,
        address pool
    ) external;
}

/// @title Equifold gen3 V3-native launch module
/// @author Equifold
/// @notice A single fixed-supply token's launch home on the SHARED Robinhood-chain Uniswap V3
///         fork. The module receives the full token allocation, seeds a single-sided concentrated
///         position (100% token, no quote) spanning from the launch tick to the top of the usable
///         range, and holds the position NFT FOREVER: there is intentionally NO function that
///         transfers, decreases, or burns it. Buyers push price up through the range like a bonding
///         curve; the position accrues the paired quote asset (WETH) which anyone can permissionlessly
///         crank via {collectFees}, split 70/30 creator/protocol using the launchpad's existing
///         fee-share machinery. "Graduation" here is a read-only milestone ({isGraduated}) — the
///         liquidity is already live and permanently locked from block one.
/// @dev The permanent lock is the ABSENCE of any exit path — audited as a product guarantee.
contract EquifoldV3LaunchModule is IStandardTokenLaunchModule, ReentrancyGuard {
    using SafeERC20 for IERC20;

    error AlreadyInitialized();
    error DeadlineExpired();
    error InvalidAddress();
    error InvalidToken();
    error NoFeesToCollect();
    error NotInitialized();
    error NotStandardTokenFactory();
    error PoolNotAtLaunchPrice();
    error UnexpectedEther();

    uint16 internal constant BPS_DENOMINATOR = 10_000;
    /// @notice Creator's share of every collected fee (quote and token side alike).
    uint16 internal constant CREATOR_BPS = 7000;
    /// @notice Protocol-remainder sub-splits of the quote side (of the 30% that is not the creator's).
    uint16 internal constant PROTOCOL_TREASURY_BPS = 4000;
    uint16 internal constant PROTOCOL_BUYBACK_BPS = 4000;
    uint16 internal constant PROTOCOL_OPERATIONS_BPS = 1000;
    /// @notice Top of the usable tick range for a 10000-fee (spacing-200) pool: (887272 / 200) * 200.
    int24 internal constant MAX_USABLE_TICK = 887_200;
    /// @notice Paired-quote principal at which the launch is considered graduated (read-only).
    uint256 internal constant GRADUATION_THRESHOLD = 3.75 ether;

    /// @dev Immutable configuration passed by {EquifoldV3LaunchModuleFactory} at creation.
    struct Initialization {
        address moduleFactory;
        address standardTokenFactory;
        address creator;
        address payable treasury;
        address feeCollector;
        IWrappedNative weth;
        address v3Factory;
        address positionManager;
        address swapRouter;
        uint256 projectNumber;
        uint24 poolFee;
        int24 initialTick;
    }

    address public immutable moduleFactory;
    address public immutable standardTokenFactory;
    address public immutable creator;
    address payable public immutable treasury;
    ILaunchpadTradingFeeCollector public immutable feeCollector;
    IWrappedNative public immutable weth;
    address public immutable v3Factory;
    address public immutable positionManager;
    address public immutable swapRouter;
    uint256 public immutable projectNumber;
    uint24 public immutable poolFee;
    /// @notice The token-perspective launch tick (`price = token per WETH`); sign-flipped at use so
    ///         the effective pool price is identical regardless of the token/WETH address sort.
    int24 public immutable initialTick;

    address public token;
    address public pool;
    uint256 public positionId;
    /// @notice Whether the launch token sorts below WETH (is currency0 of the pool).
    bool public isToken0;

    event TokenInitialized(
        uint256 indexed projectNumber,
        address indexed token,
        address indexed pool,
        uint256 positionId
    );
    event FeesCollected(uint256 indexed projectNumber, uint256 quoteAmount, uint256 tokenAmount);

    constructor(
        Initialization memory init
    ) {
        if (
            init.moduleFactory == address(0) || init.standardTokenFactory == address(0)
                || init.creator == address(0) || init.treasury == address(0)
                || init.feeCollector == address(0) || address(init.weth) == address(0)
                || init.v3Factory == address(0) || init.positionManager == address(0)
                || init.swapRouter == address(0)
        ) revert InvalidAddress();
        if (
            init.moduleFactory.code.length == 0 || init.standardTokenFactory.code.length == 0
                || init.feeCollector.code.length == 0 || address(init.weth).code.length == 0
                || init.v3Factory.code.length == 0 || init.positionManager.code.length == 0
                || init.swapRouter.code.length == 0
        ) revert InvalidAddress();
        if (init.projectNumber == 0 || init.poolFee == 0) revert InvalidAddress();

        moduleFactory = init.moduleFactory;
        standardTokenFactory = init.standardTokenFactory;
        creator = init.creator;
        treasury = init.treasury;
        feeCollector = ILaunchpadTradingFeeCollector(init.feeCollector);
        weth = init.weth;
        v3Factory = init.v3Factory;
        positionManager = init.positionManager;
        swapRouter = init.swapRouter;
        projectNumber = init.projectNumber;
        poolFee = init.poolFee;
        initialTick = init.initialTick;
    }

    /// @notice Binds the launch token, creates + initializes its pool at the launch price, and mints
    ///         the permanently-locked single-sided position holding the full supply.
    /// @dev Callable once, only by the standard-token factory (the token's `launchContract`), which
    ///      has already transferred the full supply to this module in the same transaction.
    function initializeToken(
        address token_
    ) external nonReentrant {
        if (msg.sender != standardTokenFactory) revert NotStandardTokenFactory();
        if (token != address(0)) revert AlreadyInitialized();
        if (token_ == address(0) || token_.code.length == 0) revert InvalidToken();
        uint256 supply = IERC20(token_).totalSupply();
        if (supply == 0 || IERC20(token_).balanceOf(address(this)) != supply) {
            revert InvalidToken();
        }

        bool token0 = token_ < address(weth);
        token = token_;
        isToken0 = token0;

        (address pool_, uint256 tokenId) = _openLockedPosition(token_, supply, token0);
        pool = pool_;
        positionId = tokenId;
        emit TokenInitialized(projectNumber, token_, pool_, tokenId);
        IEquifoldV3ModuleFactoryCallback(moduleFactory).notifyPoolInitialized(token_, pool_);
    }

    /// @dev Creates + initializes the pool at the launch price, verifies no hostile pre-creation,
    ///      then mints the single-sided position holding the full supply. Split out of
    ///      {initializeToken} to keep each frame's stack shallow.
    function _openLockedPosition(
        address token_,
        uint256 supply,
        bool token0
    ) private returns (address pool_, uint256 tokenId) {
        // Token-perspective tick flips with the sort so the pool always opens at the same price.
        int24 poolTick = token0 ? initialTick : -initialTick;
        (address sorted0, address sorted1) =
            token0 ? (token_, address(weth)) : (address(weth), token_);
        uint160 targetSqrtPrice = TickMath.getSqrtPriceAtTick(poolTick);

        pool_ = INonfungiblePositionManagerMinimal(positionManager)
            .createAndInitializePoolIfNecessary(sorted0, sorted1, poolFee, targetSqrtPrice);
        // Reject a hostile pre-created pool at a different price (stronger than a getPool pre-check).
        (uint160 sqrtPriceX96,,,,,,) = IUniswapV3PoolMinimal(pool_).slot0();
        if (sqrtPriceX96 != targetSqrtPrice) revert PoolNotAtLaunchPrice();

        IERC20(token_).forceApprove(positionManager, supply);
        tokenId = _mintSingleSided(sorted0, sorted1, token0, supply);
        IERC20(token_).forceApprove(positionManager, 0);
    }

    /// @dev Mints the full-supply single-sided position spanning from the launch tick to the far end
    ///      of the usable range (all token, zero quote), owned by this module forever.
    function _mintSingleSided(
        address sorted0,
        address sorted1,
        bool token0,
        uint256 supply
    ) private returns (uint256 tokenId) {
        (int24 tickLower, int24 tickUpper) =
            token0 ? (initialTick, MAX_USABLE_TICK) : (-MAX_USABLE_TICK, -initialTick);
        (uint256 amount0Desired, uint256 amount1Desired) =
            token0 ? (supply, uint256(0)) : (uint256(0), supply);
        (tokenId,,,) = INonfungiblePositionManagerMinimal(positionManager)
            .mint(
                INonfungiblePositionManagerMinimal.MintParams({
                    token0: sorted0,
                    token1: sorted1,
                    fee: poolFee,
                    tickLower: tickLower,
                    tickUpper: tickUpper,
                    amount0Desired: amount0Desired,
                    amount1Desired: amount1Desired,
                    amount0Min: 0,
                    amount1Min: 0,
                    recipient: address(this),
                    deadline: block.timestamp
                })
            );
    }

    /// @notice Executes the creator's optional first market buy atomically with launch.
    /// @dev Only the standard-token factory may call, in the same block as token deploy. Opens the
    ///      token's single-recipient launch-block exemption, routes the swap, then closes it — the
    ///      open/route/close is paired atomically so the exemption can never outlive this call.
    function initialBuy(
        uint256 minTokensOut,
        address recipient,
        uint256 deadline
    ) external payable nonReentrant returns (uint256 tokensOut) {
        if (msg.sender != standardTokenFactory) revert NotStandardTokenFactory();
        if (block.timestamp > deadline) revert DeadlineExpired();
        address token_ = token;
        if (token_ == address(0)) revert NotInitialized();

        EquifoldRestrictedToken(token_).setInitialBuyRecipient(recipient);
        tokensOut = ISwapRouter02Minimal(swapRouter).exactInputSingle{ value: msg.value }(
            ISwapRouter02Minimal.ExactInputSingleParams({
                tokenIn: address(weth),
                tokenOut: token_,
                fee: poolFee,
                recipient: recipient,
                amountIn: msg.value,
                amountOutMinimum: minTokensOut,
                sqrtPriceLimitX96: 0
            })
        );
        EquifoldRestrictedToken(token_).setInitialBuyRecipient(address(0));
    }

    /// @notice Permissionlessly cranks the locked position's accrued fees to their fixed
    ///         destinations. Quote (WETH) is unwrapped and split 70% creator / 30% protocol through
    ///         the launchpad fee collector (identical claim machinery as curve tokens); the token
    ///         side goes 70% to the creator wallet directly and the remainder to the treasury.
    /// @dev Intentionally has no access control and fixed destinations. Reverts if nothing accrued.
    function collectFees() external nonReentrant returns (uint256 quoteOut, uint256 tokenOut) {
        if (positionId == 0) revert NotInitialized();
        (uint256 amount0, uint256 amount1) = INonfungiblePositionManagerMinimal(positionManager)
            .collect(
                INonfungiblePositionManagerMinimal.CollectParams({
                    tokenId: positionId,
                    recipient: address(this),
                    amount0Max: type(uint128).max,
                    amount1Max: type(uint128).max
                })
            );
        (quoteOut, tokenOut) = isToken0 ? (amount1, amount0) : (amount0, amount1);
        if (quoteOut == 0 && tokenOut == 0) revert NoFeesToCollect();

        if (quoteOut > 0) _routeQuoteFees(quoteOut);
        if (tokenOut > 0) _routeTokenFees(tokenOut);
        emit FeesCollected(projectNumber, quoteOut, tokenOut);
    }

    /// @notice The WETH (quote) principal the permanently-locked position currently holds.
    /// @dev Zero until initialized. Grows from zero as buyers push price up through the range.
    function pairedPrincipal() public view returns (uint256 principal) {
        if (positionId == 0) return 0;
        (uint160 sqrtPriceX96,,,,,,) = IUniswapV3PoolMinimal(pool).slot0();
        (,,,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) =
            INonfungiblePositionManagerMinimal(positionManager).positions(positionId);
        (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
            sqrtPriceX96,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            liquidity
        );
        principal = isToken0 ? amount1 : amount0;
    }

    /// @notice Progress toward the (read-only) graduation milestone, in basis points, capped at 100%.
    function progressToGraduation() external view returns (uint16 bps) {
        uint256 principal = pairedPrincipal();
        if (principal >= GRADUATION_THRESHOLD) return BPS_DENOMINATOR;
        return uint16(Math.mulDiv(principal, BPS_DENOMINATOR, GRADUATION_THRESHOLD));
    }

    /// @notice Whether the paired principal has reached the graduation threshold.
    function isGraduated() external view returns (bool) {
        return pairedPrincipal() >= GRADUATION_THRESHOLD;
    }

    /// @dev Unwraps the collected WETH and forwards the whole amount to the fee collector, split
    ///      70/30. The protocol remainder's safety leg absorbs rounding dust so the FeeAmounts sum
    ///      EXACTLY equals `quoteAmount` — {LaunchpadFeeCollector.collectTradingFees} reverts otherwise.
    function _routeQuoteFees(
        uint256 quoteAmount
    ) private {
        weth.withdraw(quoteAmount);
        uint256 creatorAmount = (quoteAmount * CREATOR_BPS) / BPS_DENOMINATOR;
        uint256 protocol = quoteAmount - creatorAmount;
        uint256 treasuryAmount = (protocol * PROTOCOL_TREASURY_BPS) / BPS_DENOMINATOR;
        uint256 buybackAmount = (protocol * PROTOCOL_BUYBACK_BPS) / BPS_DENOMINATOR;
        uint256 operationsAmount = (protocol * PROTOCOL_OPERATIONS_BPS) / BPS_DENOMINATOR;
        uint256 safetyAmount = protocol - treasuryAmount - buybackAmount - operationsAmount;
        feeCollector.collectTradingFees{ value: quoteAmount }(
            projectNumber,
            true,
            ILaunchpadTradingFeeCollector.FeeAmounts({
                creator: creatorAmount,
                treasury: treasuryAmount,
                buyback: buybackAmount,
                operations: operationsAmount,
                safety: safetyAmount
            })
        );
    }

    /// @dev Splits the collected token side: 70% straight to the creator wallet, remainder to the
    ///      treasury (the remainder absorbs the rounding dust).
    function _routeTokenFees(
        uint256 tokenAmount
    ) private {
        uint256 creatorAmount = (tokenAmount * CREATOR_BPS) / BPS_DENOMINATOR;
        uint256 treasuryAmount = tokenAmount - creatorAmount;
        IERC20(token).safeTransfer(creator, creatorAmount);
        if (treasuryAmount > 0) IERC20(token).safeTransfer(treasury, treasuryAmount);
    }

    /// @notice Accepts ETH only from the WETH contract (the {collectFees} unwrap).
    receive() external payable {
        if (msg.sender != address(weth)) revert UnexpectedEther();
    }
}
Chain explorer3225msChain node83ms