LpFeeCollector
0xea2f0aef181eb38ecacd7d44e918dde0c9f857ab
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
6,746 bytes
ABI entries
42
16 read · 13 write
License
none
Contract information
- Address
- 0xea2f0aef181eb38ecacd7d44e918dde0c9f857ab
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x76053CcE1B…c8949D3385
- Creation tx
- 0xaa14837df1…7b9b9a19f3
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (16)
DEFAULT_CREATOR_FEE_BPS() → uint256
FEE_DENOM() → uint256
creatorFeeRecipient(uint256) → address
creatorOf(uint256) → address
factoryCreatorFeeBps(address) → uint256
isFactory(address) → bool
isMinter(address) → bool
minterCreatorFeeBps(address) → uint256
owner() → address
pendingCreatorFees(uint256, address) → uint256
pendingProtocolFees(address) → uint256
positionCreatorFeeBps(uint256) → uint256
positionInfo(uint256) → address, address, address, address, uint256, uint256
positionManager() → address
positionToken0(uint256) → address
positionToken1(uint256) → address
Events (8)
CreatorFeeRecipientSetCreatorFeesClaimedFactoryUpdatedFeesCollectedMinterUpdatedOwnershipTransferredPositionRegisteredProtocolFeesWithdrawn
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_positionManager",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"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"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "oldRecipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "CreatorFeeRecipientSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "CreatorFeesClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "factory",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "FactoryUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
}
],
"name": "FeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "minter",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "MinterUpdated",
"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": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "token1",
"type": "address"
}
],
"name": "PositionRegistered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "ProtocolFeesWithdrawn",
"type": "event"
},
{
"inputs": [],
"name": "DEFAULT_CREATOR_FEE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FEE_DENOM",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "claimCreatorFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "collectFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "creatorFeeRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "creatorOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "factoryCreatorFeeBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isFactory",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isMinter",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "minterCreatorFeeBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "minter",
"type": "address"
}
],
"name": "onboardMinter",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "pendingCreatorFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "pendingProtocolFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "positionCreatorFeeBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "positionInfo",
"outputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
"name": "token1",
"type": "address"
},
{
"internalType": "uint256",
"name": "pendingCreator0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "pendingCreator1",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "positionToken0",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "positionToken1",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
}
],
"name": "registerPosition",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "setCreatorFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "factory",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "setFactory",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "factory",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
},
{
"internalType": "uint256",
"name": "creatorFeeBps",
"type": "uint256"
}
],
"name": "setFactory",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "minter",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
},
{
"internalType": "uint256",
"name": "creatorFeeBps",
"type": "uint256"
}
],
"name": "setMinter",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "minter",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "setMinter",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdrawProtocolFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "tokens",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdrawProtocolFeesBatch",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./interfaces/INonfungiblePositionManager.sol";
import "./interfaces/ILpFeeCollector.sol";
/**
* @title LpFeeCollector
* @notice Holds every Uniswap V3 LP position minted by RobinLaunch — both bonding-curve
* graduations (Factory/BondingCurve) and instant Direct Pool launches (DirectFactory)
* — replacing the previous "burn the LP NFT to 0xdead" pattern. Trading fees that
* accrue on these positions after launch are now collectible and split between the
* token creator and the protocol, instead of being permanently unreachable.
*
* Security model — read this before touching anything:
* • This contract can NEVER move the underlying liquidity. There is no `decreaseLiquidity`,
* no `burn`, no NFT transfer function of any kind, and it never approves the NFT to anyone.
* Liquidity stays exactly as permanently locked as it was when sent to 0xdead — the only
* thing that changed is that swap fees are now reachable.
* • `registerPosition` can only succeed once per tokenId (enforced by `creatorOf[tokenId] ==
* address(0)`), so a position's creator can never be overwritten or hijacked later — not
* even by the contract owner.
* • Creator and protocol balances are accounted for separately, per token, exactly like
* `BondingCurve.pendingCreatorFees` / `Factory.pendingFees` today. The owner can only ever
* withdraw the protocol share; it can never touch a creator's pending balance.
* • Two independent, owner-controlled allow-lists gate write access:
* – `isFactory`: contracts allowed to onboard *new* minters (the bonding-curve Factory —
* it self-registers each BondingCurve it deploys, atomically, in the same transaction).
* – `isMinter`: contracts allowed to call `registerPosition` directly (every onboarded
* BondingCurve, plus the DirectFactory, which registers on its own behalf).
*
* Fee split: set per minter at allow-list time (bonding Factory → typically 70% creator /
* 30% protocol; DirectFactory → typically 90% / 10%). Frozen per position at registration.
*/
contract LpFeeCollector is Ownable, ReentrancyGuard, ILpFeeCollector {
using SafeERC20 for IERC20;
// ─── Constants ────────────────────────────────────────────────────────────
uint256 public constant FEE_DENOM = 10_000;
/// Fallback creator share if a minter was allow-listed without an explicit BPS.
/// Aligned with bonding-curve launches (70 % creator / 30 % protocol).
uint256 public constant DEFAULT_CREATOR_FEE_BPS = 7_000;
// ─── Immutables ───────────────────────────────────────────────────────────
INonfungiblePositionManager public immutable positionManager;
// ─── State ────────────────────────────────────────────────────────────────
/// Contracts allowed to onboard new minters (i.e. the bonding-curve Factory).
mapping(address => bool) public isFactory;
/// Creator fee BPS applied when this factory calls `onboardMinter`.
mapping(address => uint256) public factoryCreatorFeeBps;
/// Contracts allowed to call registerPosition (onboarded BondingCurves + DirectFactory).
mapping(address => bool) public isMinter;
/// Creator fee BPS for positions registered by this minter.
mapping(address => uint256) public minterCreatorFeeBps;
/// tokenId → original creator. Set exactly once at registration, never overwritten.
mapping(uint256 => address) public creatorOf;
/// tokenId → creator share of Uniswap fees (BPS of FEE_DENOM), frozen at registration.
mapping(uint256 => uint256) public positionCreatorFeeBps;
/// tokenId → payout address for creator fees. Defaults to creator, changeable by creator.
mapping(uint256 => address) public creatorFeeRecipient;
/// tokenId → pool tokens, resolved once from the position manager at registration time.
mapping(uint256 => address) public positionToken0;
mapping(uint256 => address) public positionToken1;
/// tokenId → token → creator fees collected but not yet claimed.
mapping(uint256 => mapping(address => uint256)) public pendingCreatorFees;
/// token → protocol fees collected but not yet withdrawn (aggregated across all positions).
mapping(address => uint256) public pendingProtocolFees;
// ─── Events ───────────────────────────────────────────────────────────────
event FactoryUpdated(address indexed factory, bool allowed);
event MinterUpdated(address indexed minter, bool allowed);
event PositionRegistered(uint256 indexed tokenId, address indexed creator, address token0, address token1);
event CreatorFeeRecipientSet(uint256 indexed tokenId, address indexed oldRecipient, address indexed newRecipient);
event FeesCollected(uint256 indexed tokenId, uint256 amount0, uint256 amount1);
event CreatorFeesClaimed(uint256 indexed tokenId, address indexed recipient, address token, uint256 amount);
event ProtocolFeesWithdrawn(address indexed token, address indexed to, uint256 amount);
// ─── Constructor ──────────────────────────────────────────────────────────
constructor(address _positionManager) Ownable(msg.sender) {
require(_positionManager != address(0), "LpFeeCollector: zero positionManager");
positionManager = INonfungiblePositionManager(_positionManager);
}
// ─── Owner setup (one-time, right after deployment) ──────────────────────
/**
* @notice Allow/revoke a bonding-curve Factory to onboard new minters
* (i.e. to vouch for each BondingCurve it deploys).
* Uses DEFAULT_CREATOR_FEE_BPS (70%) for onboarded curves.
*/
function setFactory(address factory, bool allowed) external onlyOwner {
_setFactory(factory, allowed, DEFAULT_CREATOR_FEE_BPS);
}
/**
* @notice Same as `setFactory(factory, allowed)` with an explicit creator fee BPS
* (e.g. 7000 = 70% creator / 30% protocol on post-graduation Uniswap fees).
*/
function setFactory(address factory, bool allowed, uint256 creatorFeeBps) external onlyOwner {
_setFactory(factory, allowed, creatorFeeBps);
}
function _setFactory(address factory, bool allowed, uint256 creatorFeeBps) internal {
require(factory != address(0), "LpFeeCollector: zero factory");
if (allowed) {
require(creatorFeeBps > 0 && creatorFeeBps <= FEE_DENOM, "LpFeeCollector: bad fee bps");
factoryCreatorFeeBps[factory] = creatorFeeBps;
}
isFactory[factory] = allowed;
emit FactoryUpdated(factory, allowed);
}
/**
* @notice Directly allow/revoke a minter (used for DirectFactory).
* Uses DEFAULT_CREATOR_FEE_BPS unless the 3-arg overload is used.
*/
function setMinter(address minter, bool allowed) external onlyOwner {
_setMinter(minter, allowed, DEFAULT_CREATOR_FEE_BPS);
}
/**
* @notice Allow/revoke a minter with an explicit creator fee BPS
* (e.g. DirectFactory → 9000 = 90% creator / 10% protocol).
*/
function setMinter(address minter, bool allowed, uint256 creatorFeeBps) external onlyOwner {
_setMinter(minter, allowed, creatorFeeBps);
}
function _setMinter(address minter, bool allowed, uint256 creatorFeeBps) internal {
require(minter != address(0), "LpFeeCollector: zero minter");
if (allowed) {
require(creatorFeeBps > 0 && creatorFeeBps <= FEE_DENOM, "LpFeeCollector: bad fee bps");
minterCreatorFeeBps[minter] = creatorFeeBps;
}
isMinter[minter] = allowed;
emit MinterUpdated(minter, allowed);
}
// ─── Factory-controlled onboarding ────────────────────────────────────────
/**
* @notice Called by an allow-listed Factory, in the same transaction as it deploys a
* new BondingCurve, so that curve can register its own position when it
* eventually graduates — with no per-token owner intervention required.
* Inherits the factory's configured creator fee BPS.
*/
function onboardMinter(address minter) external override {
require(isFactory[msg.sender], "LpFeeCollector: not factory");
require(minter != address(0), "LpFeeCollector: zero minter");
uint256 bps = factoryCreatorFeeBps[msg.sender];
if (bps == 0) bps = DEFAULT_CREATOR_FEE_BPS;
isMinter[minter] = true;
minterCreatorFeeBps[minter] = bps;
emit MinterUpdated(minter, true);
}
// ─── Registration (called once, right after mint) ────────────────────────
/**
* @notice Record the creator of a freshly-minted LP position owned by this contract.
* Must be called in the same transaction as the mint, by the minter itself.
* Freezes the minter's creator fee BPS onto this tokenId forever.
*/
function registerPosition(uint256 tokenId, address creator) external override {
require(isMinter[msg.sender], "LpFeeCollector: not minter");
require(creator != address(0), "LpFeeCollector: zero creator");
require(creatorOf[tokenId] == address(0), "LpFeeCollector: already registered");
(, , address token0, address token1, , , , , , , ,) = positionManager.positions(tokenId);
uint256 bps = minterCreatorFeeBps[msg.sender];
if (bps == 0) bps = DEFAULT_CREATOR_FEE_BPS;
creatorOf[tokenId] = creator;
creatorFeeRecipient[tokenId] = creator;
positionToken0[tokenId] = token0;
positionToken1[tokenId] = token1;
positionCreatorFeeBps[tokenId] = bps;
emit PositionRegistered(tokenId, creator, token0, token1);
}
// ─── Creator fee recipient management ─────────────────────────────────────
/**
* @notice Change the address that receives this position's creator fee payouts.
* Only callable by the original creator recorded at registration.
*/
function setCreatorFeeRecipient(uint256 tokenId, address newRecipient) external {
require(msg.sender == creatorOf[tokenId], "LpFeeCollector: not creator");
require(newRecipient != address(0), "LpFeeCollector: zero address");
address old = creatorFeeRecipient[tokenId];
creatorFeeRecipient[tokenId] = newRecipient;
emit CreatorFeeRecipientSet(tokenId, old, newRecipient);
}
// ─── Harvest ───────────────────────────────────────────────────────────────
/**
* @notice Pull all Uniswap V3 swap fees accrued on `tokenId` into this contract,
* then book the creator/protocol split frozen on this position. Permissionless — anyone can
* trigger a harvest (the "Claim" button, a keeper, or a curious observer).
*/
function collectFees(uint256 tokenId) external nonReentrant {
require(creatorOf[tokenId] != address(0), "LpFeeCollector: not registered");
(uint256 amount0, uint256 amount1) = positionManager.collect(
INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
if (amount0 > 0) _bookFees(tokenId, positionToken0[tokenId], amount0);
if (amount1 > 0) _bookFees(tokenId, positionToken1[tokenId], amount1);
emit FeesCollected(tokenId, amount0, amount1);
}
function _bookFees(uint256 tokenId, address token, uint256 amount) internal {
uint256 bps = positionCreatorFeeBps[tokenId];
if (bps == 0) bps = DEFAULT_CREATOR_FEE_BPS;
uint256 creatorShare = amount * bps / FEE_DENOM;
uint256 protocolShare = amount - creatorShare;
pendingCreatorFees[tokenId][token] += creatorShare;
pendingProtocolFees[token] += protocolShare;
}
// ─── Claim ─────────────────────────────────────────────────────────────────
/**
* @notice Claim the creator's pending balance (both pool tokens at once) for `tokenId`.
* Callable by anyone — funds always go to creatorFeeRecipient. Call collectFees()
* first to make sure the freshest fees are included.
*/
function claimCreatorFees(uint256 tokenId) external nonReentrant {
address creator = creatorOf[tokenId];
require(creator != address(0), "LpFeeCollector: not registered");
address recipient = creatorFeeRecipient[tokenId];
address t0 = positionToken0[tokenId];
address t1 = positionToken1[tokenId];
uint256 amount0 = pendingCreatorFees[tokenId][t0];
uint256 amount1 = pendingCreatorFees[tokenId][t1];
require(amount0 > 0 || amount1 > 0, "LpFeeCollector: nothing to claim");
if (amount0 > 0) {
pendingCreatorFees[tokenId][t0] = 0;
IERC20(t0).safeTransfer(recipient, amount0);
emit CreatorFeesClaimed(tokenId, recipient, t0, amount0);
}
if (amount1 > 0) {
pendingCreatorFees[tokenId][t1] = 0;
IERC20(t1).safeTransfer(recipient, amount1);
emit CreatorFeesClaimed(tokenId, recipient, t1, amount1);
}
}
// ─── Protocol fee management ───────────────────────────────────────────────
/**
* @notice Withdraw all accumulated protocol fees of `token` to `to`.
*/
function withdrawProtocolFees(address token, address to) external onlyOwner nonReentrant {
require(to != address(0), "LpFeeCollector: zero recipient");
uint256 amount = pendingProtocolFees[token];
require(amount > 0, "LpFeeCollector: no fees");
pendingProtocolFees[token] = 0;
IERC20(token).safeTransfer(to, amount);
emit ProtocolFeesWithdrawn(token, to, amount);
}
/**
* @notice Convenience batch withdrawal across several tokens in one call.
*/
function withdrawProtocolFeesBatch(address[] calldata tokens, address to) external onlyOwner nonReentrant {
require(to != address(0), "LpFeeCollector: zero recipient");
for (uint256 i = 0; i < tokens.length; i++) {
address token = tokens[i];
uint256 amount = pendingProtocolFees[token];
if (amount == 0) continue;
pendingProtocolFees[token] = 0;
IERC20(token).safeTransfer(to, amount);
emit ProtocolFeesWithdrawn(token, to, amount);
}
}
// ─── View helpers ──────────────────────────────────────────────────────────
/**
* @notice One-call snapshot of a position for the frontend: creator, payout
* address, both pool tokens, and both pending creator balances.
*/
function positionInfo(uint256 tokenId)
external
view
returns (
address creator,
address recipient,
address token0,
address token1,
uint256 pendingCreator0,
uint256 pendingCreator1
)
{
creator = creatorOf[tokenId];
recipient = creatorFeeRecipient[tokenId];
token0 = positionToken0[tokenId];
token1 = positionToken1[tokenId];
pendingCreator0 = pendingCreatorFees[tokenId][token0];
pendingCreator1 = pendingCreatorFees[tokenId][token1];
}
}
Chain explorer1187msChain node79ms