SushiV3FeeManager

0x2bbdf7cb9e4e361006708693795b89cb1a48b1b2

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
12,455 bytes
ABI entries
41
17 read · 9 write
License
none

Contract information

Address
0x2bbdf7cb9e4e361006708693795b89cb1a48b1b2
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x922e92ea69…16ecdd990c

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (17)

BPS()uint256
MAX_CONVERSION_SLIPPAGE_BPS()uint256
POOL_FEE()uint24
TICK_SPACING()int24
TOTAL_SUPPLY()uint256
claimable(bytes32, address)uint256, uint256
config()address
factory()address
getPositionTokenId(bytes32, uint256)uint256
getPositionTokenIds(bytes32)uint256[5]
getPositionsMinted(bytes32)bool[5]
owner()address
pendingETH(bytes32, address)uint256
pendingToken(bytes32, address)uint256
positionManager()address
positionMinted(bytes32, uint256)bool
weth()address

Events (6)

FactorySetFeesAccruedFeesClaimedFeesCollectedOwnershipTransferredPositionMinted

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract SushiConfig",
        "name": "_config",
        "type": "address"
      },
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "_positionManager",
        "type": "address"
      },
      {
        "internalType": "contract IWETH9",
        "name": "_weth",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_owner",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyRegistered",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FactoryAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyFactory",
    "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": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "SafeCastOverflowedUintToInt",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "factory",
        "type": "address"
      }
    ],
    "name": "FactorySet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "FeesAccrued",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "FeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethGained",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenGained",
        "type": "uint256"
      }
    ],
    "name": "FeesCollected",
    "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": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "liquidity",
        "type": "uint128"
      }
    ],
    "name": "PositionMinted",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_CONVERSION_SLIPPAGE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POOL_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "claim",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "claimable",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "positionIndex",
        "type": "uint256"
      }
    ],
    "name": "collectFeesFromPosition",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "config",
    "outputs": [
      {
        "internalType": "contract SushiConfig",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      }
    ],
    "name": "devBuy",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "factory",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "getPositionTokenId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "getPositionTokenIds",
    "outputs": [
      {
        "internalType": "uint256[5]",
        "name": "",
        "type": "uint256[5]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "getPositionsMinted",
    "outputs": [
      {
        "internalType": "bool[5]",
        "name": "",
        "type": "bool[5]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "pendingETH",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "pendingToken",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "positionMinted",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "bytes32",
            "name": "poolId",
            "type": "bytes32"
          },
          {
            "internalType": "address",
            "name": "token",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "pool",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "beneficiary",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "targetMcapETH",
            "type": "uint256"
          },
          {
            "internalType": "enum SushiV3FeeManager.FeeClaimMode",
            "name": "feeClaimMode",
            "type": "uint8"
          },
          {
            "components": [
              {
                "internalType": "address[5]",
                "name": "wallets",
                "type": "address[5]"
              },
              {
                "internalType": "uint16[5]",
                "name": "percentages",
                "type": "uint16[5]"
              },
              {
                "internalType": "uint8",
                "name": "count",
                "type": "uint8"
              }
            ],
            "internalType": "struct SushiV3FeeManager.FeeSplit",
            "name": "feeSplit",
            "type": "tuple"
          },
          {
            "internalType": "uint256",
            "name": "bootstrapETH",
            "type": "uint256"
          }
        ],
        "internalType": "struct SushiV3FeeManager.RegisterParams",
        "name": "p",
        "type": "tuple"
      }
    ],
    "name": "registerAndMint",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rescueETH",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_factory",
        "type": "address"
      }
    ],
    "name": "setFactory",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "int256",
        "name": "amount0Delta",
        "type": "int256"
      },
      {
        "internalType": "int256",
        "name": "amount1Delta",
        "type": "int256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "uniswapV3SwapCallback",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "contract IWETH9",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "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 {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import {
    IUniswapV3Pool,
    INonfungiblePositionManager,
    IUniswapV3SwapCallback,
    IWETH9
} from "../interfaces/ISushiV3.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SushiConfig} from "./SushiConfig.sol";
import {PriceMath} from "../libraries/PriceMath.sol";
import {LiquidityAmounts} from "../libraries/math/LiquidityAmounts.sol";
import {TickMath} from "../libraries/math/TickMath.sol";
import {FullMath} from "../libraries/math/FullMath.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

/**
 * @title SushiV3FeeManager
 * @notice Position + swap + fee half of the Sushi V3 launcher (the counterpart of
 *         the original Uniswap-V4 Core+Helper split, resurrected here purely to
 *         keep each contract under the 24KB EIP-170 limit).
 *
 *         The SushiV3Factory orchestrates launches (token deploy, pool create/init,
 *         registry) and, per launch, hands this contract the full token supply plus
 *         the launch parameters via `registerAndMint`. From that point on this
 *         contract:
 *           - holds the 5 concentrated NFT positions (it is the NPM `recipient`),
 *           - is the pool `swap` caller for the atomic dev-buy and fee conversions,
 *           - collects + splits LP fees (20% platform / 80% beneficiary),
 *           - exposes the ProofOfSteak fee-source interface
 *             (collectFeesFromPosition / claim / claimable) unchanged.
 *
 *         ONLY the factory may call `registerAndMint` and `devBuy`. Fee collection
 *         and claims are permissionless (interface-identical to the DIRECT stack so
 *         ProofOfSteak's harvest loop works verbatim).
 */
contract SushiV3FeeManager is Ownable, ReentrancyGuard, IUniswapV3SwapCallback {
    using SafeCast for uint256;

    uint256 public constant TOTAL_SUPPLY = 1_000_000_000 * 1e18;
    uint256 public constant BPS = 10000;

    uint24 public constant POOL_FEE = 10000;
    int24 public constant TICK_SPACING = 200;

    /// @notice Max slippage (BPS vs pre-swap spot) tolerated on fee-conversion
    ///         swaps (ETH_ONLY / TOKEN_ONLY). Loose (50%) — a thin launch pool
    ///         legitimately loses a lot to pool fee + impact; the guard only blocks
    ///         an egregious sandwich.
    uint256 public constant MAX_CONVERSION_SLIPPAGE_BPS = 5000;

    enum FeeClaimMode { BOTH, ETH_ONLY, TOKEN_ONLY }

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

    /// @notice Everything this contract needs per pool. Populated once by the
    ///         factory's `registerAndMint` call during a launch.
    struct PoolRecord {
        address token;
        address pool;
        address beneficiary;
        uint256 targetMcapETH;
        FeeClaimMode feeClaimMode;
        FeeSplit feeSplit;
        uint256[5] positionTokenIds;
        bool[5] positionMinted;
        bool registered;
    }

    /// @notice Launch parameters passed by the factory in one struct (avoids a
    ///         stack-too-deep argument list).
    struct RegisterParams {
        bytes32 poolId;
        address token;
        address pool;
        address beneficiary;
        uint256 targetMcapETH;
        FeeClaimMode feeClaimMode;
        FeeSplit feeSplit;
        uint256 bootstrapETH;
    }

    SushiConfig public immutable config;
    INonfungiblePositionManager public immutable positionManager;
    IWETH9 public immutable weth;

    /// @notice The one factory allowed to register launches + trigger dev-buys.
    address public factory;

    mapping(bytes32 => PoolRecord) internal _records;

    /// @notice Unclaimed ETH / token fee share per (poolId, recipient).
    mapping(bytes32 => mapping(address => uint256)) public pendingETH;
    mapping(bytes32 => mapping(address => uint256)) public pendingToken;

    /// @dev Transient guard so uniswapV3SwapCallback only trusts calls from the
    ///      pool we are actively swapping against in this tx.
    address private _activeSwapPool;

    event FactorySet(address indexed factory);
    event PositionMinted(bytes32 indexed poolId, uint256 index, uint256 tokenId, uint128 liquidity);
    event FeesCollected(bytes32 indexed poolId, uint256 positionIndex, uint256 ethGained, uint256 tokenGained);
    event FeesAccrued(bytes32 indexed poolId, address indexed recipient, uint256 ethAmount, uint256 tokenAmount);
    event FeesClaimed(bytes32 indexed poolId, address indexed recipient, uint256 ethAmount, uint256 tokenAmount);

    error OnlyFactory();
    error FactoryAlreadySet();
    error AlreadyRegistered();

    constructor(
        SushiConfig _config,
        INonfungiblePositionManager _positionManager,
        IWETH9 _weth,
        address _owner
    ) Ownable(_owner) {
        config = _config;
        positionManager = _positionManager;
        weth = _weth;
    }

    modifier onlyFactory() {
        if (msg.sender != factory) revert OnlyFactory();
        _;
    }

    /// @notice One-time wiring of the factory allowed to register launches. Owner
    ///         gated; can only be set once (immutability after deploy wiring).
    function setFactory(address _factory) external onlyOwner {
        require(_factory != address(0), "Zero factory");
        if (factory != address(0)) revert FactoryAlreadySet();
        factory = _factory;
        emit FactorySet(_factory);
    }

    // ------------------------------------------------------------------
    // REGISTRATION + LIQUIDITY (factory-only)
    // ------------------------------------------------------------------

    /// @notice Register a launch and mint its 5 bootstrap positions. The factory
    ///         MUST have transferred the full token supply to this contract and
    ///         exempted this contract from token limits before calling. The
    ///         `bootstrapETH` is forwarded as msg.value and wrapped to WETH here.
    function registerAndMint(RegisterParams calldata p) external payable onlyFactory nonReentrant {
        PoolRecord storage r = _records[p.poolId];
        if (r.registered) revert AlreadyRegistered();

        r.token = p.token;
        r.pool = p.pool;
        r.beneficiary = p.beneficiary;
        r.targetMcapETH = p.targetMcapETH;
        r.feeClaimMode = p.feeClaimMode;
        r.feeSplit = p.feeSplit;
        r.registered = true;

        require(msg.value == p.bootstrapETH, "Bootstrap mismatch");

        _mintPositions(p.poolId, p.bootstrapETH);
    }

    /// @notice Atomic dev-buy (WETH -> token) delivered to `recipient` (the
    ///         creator). Factory-only; forwards `ethIn` as msg.value.
    function devBuy(bytes32 poolId, address recipient, uint256 ethIn)
        external
        payable
        onlyFactory
        nonReentrant
    {
        require(msg.value == ethIn, "DevBuy mismatch");
        PoolRecord storage r = _records[poolId];
        address token = r.token;

        // Wrap ETH; the callback will pay WETH to the pool.
        weth.deposit{value: ethIn}();

        // WETH(token0) -> token(token1): zeroForOne = true, exact-input.
        uint256 tokensOut = _poolSwap(r.pool, token, true, ethIn, 0);

        if (tokensOut > 0) {
            IERC20(token).transfer(recipient, tokensOut);
        }
    }

    function _mintPositions(bytes32 poolId, uint256 bootstrapETH) internal {
        PoolRecord storage r = _records[poolId];
        address token = r.token;

        (int24[5] memory tickLowers, int24[5] memory tickUppers, uint256[5] memory allocations) =
            PriceMath.calculatePositionRanges(r.targetMcapETH, TOTAL_SUPPLY, TICK_SPACING);

        (uint160 curSqrtPrice,,,,,,) = IUniswapV3Pool(r.pool).slot0();

        // Wrap the whole bootstrap into WETH once and approve NPM for both assets.
        if (bootstrapETH > 0) {
            weth.deposit{value: bootstrapETH}();
            weth.approve(address(positionManager), bootstrapETH);
        }
        // Approve the NPM to pull the token allocations.
        IERC20(token).approve(address(positionManager), TOTAL_SUPPLY);

        uint256 remainingWeth = bootstrapETH;

        for (uint256 i = 0; i < 5; i++) {
            uint160 sqrtLower = TickMath.getSqrtPriceAtTick(tickLowers[i]);
            uint160 sqrtUpper = TickMath.getSqrtPriceAtTick(tickUppers[i]);

            // WETH = token0. A position needs WETH only while the current price is
            // below its upper bound; token-only (out-of-range above) positions take
            // no WETH. At launch only P1 is in range, so it receives all the WETH.
            uint256 wethForPosition = curSqrtPrice < sqrtUpper ? remainingWeth : 0;

            uint128 liq = LiquidityAmounts.getLiquidityForAmounts(
                curSqrtPrice, sqrtLower, sqrtUpper, wethForPosition, allocations[i]
            );
            if (liq == 0) continue;

            // token0 = WETH, token1 = token (orientation enforced by the factory).
            INonfungiblePositionManager.MintParams memory mp = INonfungiblePositionManager.MintParams({
                token0: address(weth),
                token1: token,
                fee: POOL_FEE,
                tickLower: tickLowers[i],
                tickUpper: tickUppers[i],
                amount0Desired: wethForPosition,
                amount1Desired: allocations[i],
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this),
                deadline: block.timestamp + 1 hours
            });

            (uint256 tokenId, uint128 mintedLiq, uint256 usedWeth,) = positionManager.mint(mp);

            r.positionTokenIds[i] = tokenId;
            r.positionMinted[i] = true;
            if (usedWeth <= remainingWeth) remainingWeth -= usedWeth;

            emit PositionMinted(poolId, i, tokenId, mintedLiq);
        }

        // Any WETH the positions didn't consume: unwrap and park with this contract
        // (kept as ETH; recyclable / withdrawable by owner via rescue).
        uint256 leftover = weth.balanceOf(address(this));
        if (leftover > 0) {
            weth.withdraw(leftover);
        }
    }

    // ------------------------------------------------------------------
    // DEV-BUY & SWAPS (direct V3 pool.swap + uniswapV3SwapCallback)
    // ------------------------------------------------------------------

    /// @dev Exact-input swap directly against the pool, with a spot-price min-out
    ///      guard (fee-conversion for ETH_ONLY / TOKEN_ONLY). `wethToToken`:
    ///      WETH->token. Input (WETH or token) is already held by this contract.
    function _swapExactIn(bytes32 poolId, bool wethToToken, uint256 amountIn)
        internal
        returns (uint256 amountOut)
    {
        if (amountIn == 0) return 0;
        PoolRecord storage r = _records[poolId];

        // spot: sqrtP^2 / 2^192 = token1/token0 = token per WETH (WETH=token0).
        (uint160 sqrtP,,,,,,) = IUniswapV3Pool(r.pool).slot0();
        uint256 priceX192 = uint256(sqrtP) * uint256(sqrtP);
        uint256 expectedOut = wethToToken
            ? FullMath.mulDiv(amountIn, priceX192, 1 << 192)
            : FullMath.mulDiv(amountIn, 1 << 192, priceX192);
        uint256 minOut = (expectedOut * (BPS - MAX_CONVERSION_SLIPPAGE_BPS)) / BPS;

        amountOut = _poolSwap(r.pool, r.token, wethToToken, amountIn, minOut);
    }

    /// @dev Core exact-input pool swap. `zeroForOne == wethToToken` (WETH=token0).
    ///      Returns the output amount and enforces `minOut`.
    function _poolSwap(address pool, address token, bool wethToToken, uint256 amountIn, uint256 minOut)
        internal
        returns (uint256 amountOut)
    {
        _activeSwapPool = pool;

        // Encode which token this contract must pay in the callback.
        bytes memory cb = abi.encode(pool, token);

        uint160 limit = wethToToken ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1;

        (int256 amount0, int256 amount1) = IUniswapV3Pool(pool).swap(
            address(this), wethToToken, amountIn.toInt256(), limit, cb
        );

        // Output is the negative delta on the OUT side. WETH=token0, token=token1.
        // wethToToken: pay token0 (positive amount0), receive token1 (negative amount1).
        amountOut = wethToToken ? uint256(-amount1) : uint256(-amount0);

        _activeSwapPool = address(0);
        require(amountOut >= minOut, "Conversion slippage");
    }

    /// @notice V3 swap callback — pay the pool the input token it is owed.
    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data)
        external
        override
    {
        require(msg.sender == _activeSwapPool && msg.sender != address(0), "Unexpected callback");
        (address pool, address token) = abi.decode(data, (address, address));
        require(pool == msg.sender, "Bad pool");

        // Positive delta = amount this contract owes the pool. token0 = WETH,
        // token1 = the launch token.
        if (amount0Delta > 0) {
            // owe WETH
            weth.transfer(msg.sender, uint256(amount0Delta));
        }
        if (amount1Delta > 0) {
            // owe launch token
            IERC20(token).transfer(msg.sender, uint256(amount1Delta));
        }
    }

    // ------------------------------------------------------------------
    // FEE COLLECTION (ProofOfSteak-compatible interface)
    // ------------------------------------------------------------------

    /// @notice Collect fees from position `positionIndex` and split platform /
    ///         beneficiary. ProofOfSteak calls this (interface-identical to DIRECT).
    function collectFeesFromPosition(bytes32 poolId, uint256 positionIndex) external nonReentrant {
        _collect(poolId, positionIndex);
    }

    function _collect(bytes32 poolId, uint256 positionIndex) internal {
        PoolRecord storage r = _records[poolId];
        require(r.positionMinted[positionIndex], "Position not minted");
        uint256 tokenId = r.positionTokenIds[positionIndex];
        require(tokenId != 0, "No position");

        address token = r.token;
        FeeClaimMode mode = r.feeClaimMode;

        // Collect all owed fees (WETH + token) to this contract.
        (uint256 wethGained, uint256 tokenGained) = positionManager.collect(
            INonfungiblePositionManager.CollectParams({
                tokenId: tokenId,
                recipient: address(this),
                amount0Max: type(uint128).max,
                amount1Max: type(uint128).max
            })
        );

        // token0 = WETH -> unwrap to native ETH so downstream stays ETH-denominated.
        uint256 ethGained = wethGained;
        if (wethGained > 0) {
            weth.withdraw(wethGained);
        }

        // Convert the unwanted side per claim mode.
        if (mode == FeeClaimMode.ETH_ONLY && tokenGained > 0) {
            uint256 extraWeth = _swapExactIn(poolId, false, tokenGained); // token -> WETH
            if (extraWeth > 0) {
                weth.withdraw(extraWeth);
                ethGained += extraWeth;
            }
            tokenGained = 0;
        } else if (mode == FeeClaimMode.TOKEN_ONLY && ethGained > 0) {
            // wrap the ETH we just unwrapped, then WETH -> token
            weth.deposit{value: ethGained}();
            uint256 extraToken = _swapExactIn(poolId, true, ethGained); // WETH -> token
            tokenGained += extraToken;
            ethGained = 0;
        }

        uint256 platformBps = config.platformShareBps();

        if (ethGained > 0) {
            uint256 beneficiaryETH = (ethGained * (BPS - platformBps)) / BPS;
            uint256 platformETH = ethGained - beneficiaryETH;
            if (platformETH > 0) {
                (bool ok,) = config.treasury().call{value: platformETH}("");
                require(ok, "Platform ETH transfer failed");
            }
            if (beneficiaryETH > 0) _accrueETH(poolId, r, beneficiaryETH);
        }

        if (tokenGained > 0) {
            uint256 beneficiaryTokens = (tokenGained * (BPS - platformBps)) / BPS;
            uint256 platformTokens = tokenGained - beneficiaryTokens;
            if (platformTokens > 0) {
                IERC20(token).transfer(config.treasury(), platformTokens);
            }
            if (beneficiaryTokens > 0) _accrueToken(poolId, r, beneficiaryTokens);
        }

        emit FeesCollected(poolId, positionIndex, ethGained, tokenGained);
    }

    /// @dev Accrue ETH across the fee-split wallets (or the sole beneficiary).
    function _accrueETH(bytes32 poolId, PoolRecord storage r, uint256 amount) internal {
        if (r.feeSplit.count == 0) {
            pendingETH[poolId][r.beneficiary] += amount;
            emit FeesAccrued(poolId, r.beneficiary, amount, 0);
            return;
        }
        uint256 distributed;
        for (uint256 i = 0; i < r.feeSplit.count; i++) {
            uint256 share = (amount * r.feeSplit.percentages[i]) / BPS;
            if (i == r.feeSplit.count - 1) share = amount - distributed; // dust to last
            distributed += share;
            pendingETH[poolId][r.feeSplit.wallets[i]] += share;
            emit FeesAccrued(poolId, r.feeSplit.wallets[i], share, 0);
        }
    }

    function _accrueToken(bytes32 poolId, PoolRecord storage r, uint256 amount) internal {
        if (r.feeSplit.count == 0) {
            pendingToken[poolId][r.beneficiary] += amount;
            emit FeesAccrued(poolId, r.beneficiary, 0, amount);
            return;
        }
        uint256 distributed;
        for (uint256 i = 0; i < r.feeSplit.count; i++) {
            uint256 share = (amount * r.feeSplit.percentages[i]) / BPS;
            if (i == r.feeSplit.count - 1) share = amount - distributed;
            distributed += share;
            pendingToken[poolId][r.feeSplit.wallets[i]] += share;
            emit FeesAccrued(poolId, r.feeSplit.wallets[i], 0, share);
        }
    }

    /// @notice Claim caller's accrued share. Interface-identical to DIRECT so
    ///         ProofOfSteak's harvest loop (collectFeesFromPosition + claim) works.
    function claim(bytes32 poolId) external nonReentrant returns (uint256 ethAmount, uint256 tokenAmount) {
        PoolRecord storage r = _records[poolId];
        address token = r.token;

        ethAmount = pendingETH[poolId][msg.sender];
        tokenAmount = pendingToken[poolId][msg.sender];
        require(ethAmount > 0 || tokenAmount > 0, "Nothing to claim");

        if (ethAmount > 0) {
            pendingETH[poolId][msg.sender] = 0;
            (bool ok,) = msg.sender.call{value: ethAmount}("");
            require(ok, "Claim ETH transfer failed");
        }
        if (tokenAmount > 0) {
            pendingToken[poolId][msg.sender] = 0;
            IERC20(token).transfer(msg.sender, tokenAmount);
        }
        emit FeesClaimed(poolId, msg.sender, ethAmount, tokenAmount);
    }

    function claimable(bytes32 poolId, address recipient) external view returns (uint256, uint256) {
        return (pendingETH[poolId][recipient], pendingToken[poolId][recipient]);
    }

    // ------------------------------------------------------------------
    // VIEWS
    // ------------------------------------------------------------------

    function getPositionTokenId(bytes32 poolId, uint256 index) external view returns (uint256) {
        return _records[poolId].positionTokenIds[index];
    }

    function positionMinted(bytes32 poolId, uint256 index) external view returns (bool) {
        return _records[poolId].positionMinted[index];
    }

    function getPositionTokenIds(bytes32 poolId) external view returns (uint256[5] memory) {
        return _records[poolId].positionTokenIds;
    }

    function getPositionsMinted(bytes32 poolId) external view returns (bool[5] memory) {
        return _records[poolId].positionMinted;
    }

    /// @notice Rescue parked ETH (leftover unwrapped bootstrap) to treasury.
    function rescueETH() external onlyOwner {
        uint256 bal = address(this).balance;
        (bool ok,) = config.treasury().call{value: bal}("");
        require(ok, "rescue failed");
    }

    receive() external payable {}
}
Chain explorer3356msChain node99ms