FeeLockerV4
0xc98182af3f81ec8a3e336ce6526bfa25b9ecc610
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
14,192 bytes
ABI entries
40
10 read · 9 write
License
none
Contract information
- Address
- 0xc98182af3f81ec8a3e336ce6526bfa25b9ecc610
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xfE3e3512b7…E5331fFF83
- Creation tx
- 0xe3813e5415…c8535cb455
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (10)
DEAD() → address
MAX_PAYOUT_WALLETS() → uint256
factory() → address
getLock(address) → bool, uint128, address, address, uint16, address[], uint16[]
isGraduated(address) → bool
liquidityOf(address) → uint128
lockTicks(address) → int24, int24
poolManager() → address
withdrawableETH(address) → uint256
withdrawableToken(address, address) → uint256
Events (8)
CommunityTakeoverCreatorTransferredETHWithdrawnFeesCollectedPayoutConfigSetPositionGraduatedPositionLockedTokenWithdrawn
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "factory_",
"type": "address"
},
{
"internalType": "address",
"name": "poolManager_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyGraduated",
"type": "error"
},
{
"inputs": [],
"name": "AlreadyLocked",
"type": "error"
},
{
"inputs": [],
"name": "BadPayoutConfig",
"type": "error"
},
{
"inputs": [],
"name": "NotCreator",
"type": "error"
},
{
"inputs": [],
"name": "NotFactory",
"type": "error"
},
{
"inputs": [],
"name": "NotPoolManager",
"type": "error"
},
{
"inputs": [],
"name": "NotTreasury",
"type": "error"
},
{
"inputs": [],
"name": "NothingToWithdraw",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "UnknownToken",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "oldCreator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newCreator",
"type": "address"
}
],
"name": "CommunityTakeover",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "oldCreator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newCreator",
"type": "address"
}
],
"name": "CreatorTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "ETHWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
}
],
"name": "FeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "address[]",
"name": "wallets",
"type": "address[]"
},
{
"indexed": false,
"internalType": "uint16[]",
"name": "weights",
"type": "uint16[]"
}
],
"name": "PayoutConfigSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint128",
"name": "newLiquidity",
"type": "uint128"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethToLp",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensToLp",
"type": "uint256"
}
],
"name": "PositionGraduated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
}
],
"name": "PositionLocked",
"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": "TokenWithdrawn",
"type": "event"
},
{
"inputs": [],
"name": "DEAD",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_PAYOUT_WALLETS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "collectFees",
"outputs": [
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newCreator",
"type": "address"
}
],
"name": "communityTakeover",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "getLock",
"outputs": [
{
"internalType": "bool",
"name": "exists",
"type": "bool"
},
{
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "treasury",
"type": "address"
},
{
"internalType": "uint16",
"name": "creatorSplitBps",
"type": "uint16"
},
{
"internalType": "address[]",
"name": "payoutWallets",
"type": "address[]"
},
{
"internalType": "uint16[]",
"name": "payoutWeights",
"type": "uint16[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "int24",
"name": "newTickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "newTickUpper",
"type": "int24"
}
],
"name": "graduateRerange",
"outputs": [
{
"internalType": "uint256",
"name": "ethToLp",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokensToLp",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "isGraduated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "liquidityOf",
"outputs": [
{
"internalType": "uint128",
"name": "",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "treasury",
"type": "address"
},
{
"internalType": "uint16",
"name": "creatorSplitBps",
"type": "uint16"
}
],
"name": "lockLiquidity",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "lockTicks",
"outputs": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address[]",
"name": "wallets",
"type": "address[]"
},
{
"internalType": "uint16[]",
"name": "weights",
"type": "uint16[]"
}
],
"name": "setPayoutConfig",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newCreator",
"type": "address"
}
],
"name": "transferCreator",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "withdrawETH",
"outputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "withdrawToken",
"outputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "withdrawableETH",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "withdrawableToken",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
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 {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {ModifyLiquidityParams} from "v4-core/src/types/PoolOperation.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "v4-core/src/types/BalanceDelta.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {CurrencySettler} from "v4-core/test/utils/CurrencySettler.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol";
interface ILaunchFactoryV4 {
function isPool(address token) external view returns (bool);
}
/// @title FeeLockerV4
/// @notice Holds every Foundry launch's Uniswap V4 liquidity position FOREVER. The full 1B supply is
/// placed as a single-sided position OWNED BY THIS CONTRACT (positions in V4 are keyed by the address
/// that calls `modifyLiquidity`, so the locker itself is the owner). There is deliberately NO function
/// that can remove, transfer or burn the liquidity — the only thing the locker can do with a position
/// is `collectFees`, which pokes it (`modifyLiquidity` with liquidityDelta 0) and takes the accrued
/// LP-tier fees. That permanent, withdraw-less custody is both the "liquidity locked forever" claim and
/// the security model — the analog of the V3 `FeeLocker` holding the LP NFT forever.
///
/// @dev The pool's own LP-fee-tier fees (SEPARATE from the 1% policy fee the FoundryHookV4 skims on
/// every swap) accrue to this locked position and are split creator/protocol 50/50 by `collectFees`,
/// with the creator half fanned across up to 10 payout wallets (pump-parity). All payouts are pull
/// (credit + withdraw). Rounding dust accrues to the protocol treasury. The native-ETH side is taken
/// directly as ETH (no WETH unwrap needed — currency0 is address(0)). This mirrors the V3 FeeLocker's
/// post-graduation behaviour exactly.
contract FeeLockerV4 is IUnlockCallback, ReentrancyGuard {
using SafeERC20 for IERC20;
using PoolIdLibrary for PoolKey;
using CurrencyLibrary for Currency;
using CurrencySettler for Currency;
using BalanceDeltaLibrary for BalanceDelta;
using StateLibrary for IPoolManager;
uint256 public constant MAX_PAYOUT_WALLETS = 10;
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
address public immutable factory;
IPoolManager public immutable poolManager;
struct Lock {
bool exists;
bool graduated; // true once the curve position has been re-ranged to full-range (exactly-once)
PoolKey key;
int24 tickLower;
int24 tickUpper;
uint128 liquidity;
address creator;
address treasury;
uint16 creatorSplitBps; // creator's share of collected LP fees (5000 = 50%)
address[] payoutWallets;
uint16[] payoutWeights; // bps, sum == 10000
}
mapping(address => Lock) internal _locks; // launch token => lock
// Pull-payment ledgers.
mapping(address => uint256) public withdrawableETH;
mapping(address => mapping(address => uint256)) public withdrawableToken;
// Transient unlock context.
enum Action {
Lock,
Collect,
Graduate
}
event PositionLocked(address indexed token, uint128 liquidity, address indexed creator);
/// @dev The curve position was re-ranged into a full-range, deep, still-locked position.
event PositionGraduated(address indexed token, uint128 newLiquidity, uint256 ethToLp, uint256 tokensToLp);
event FeesCollected(address indexed token, uint256 tokenAmount, uint256 ethAmount);
event CreatorTransferred(address indexed token, address indexed oldCreator, address indexed newCreator);
event CommunityTakeover(address indexed token, address indexed oldCreator, address indexed newCreator);
event PayoutConfigSet(address indexed token, address[] wallets, uint16[] weights);
event ETHWithdrawn(address indexed to, uint256 amount);
event TokenWithdrawn(address indexed token, address indexed to, uint256 amount);
error NotFactory();
error NotPoolManager();
error AlreadyLocked();
error AlreadyGraduated();
error UnknownToken();
error NotCreator();
error NotTreasury();
error BadPayoutConfig();
error NothingToWithdraw();
constructor(address factory_, address poolManager_) {
factory = factory_;
poolManager = IPoolManager(poolManager_);
}
/// @dev Only accept ETH from the PoolManager (fee takes / liquidity settlement).
receive() external payable {
if (msg.sender != address(poolManager)) revert NotPoolManager();
}
// ─────────────────────────── locking (custody in) ───────────────────────────
/// @notice Factory-only. Places `liquidity` for `token` as the single-sided position and records the
/// lock. The factory must have already transferred the launch tokens to this locker; the locker
/// settles what the position needs and burns any dust to DEAD. There is no matching exit — once
/// locked, the liquidity can never leave.
function lockLiquidity(
PoolKey calldata key,
int24 tickLower,
int24 tickUpper,
uint128 liquidity,
address token,
address creator,
address treasury,
uint16 creatorSplitBps
) external {
if (msg.sender != factory) revert NotFactory();
if (_locks[token].exists) revert AlreadyLocked();
Lock storage l = _locks[token];
l.exists = true;
l.key = key;
l.tickLower = tickLower;
l.tickUpper = tickUpper;
l.liquidity = liquidity;
l.creator = creator;
l.treasury = treasury;
l.creatorSplitBps = creatorSplitBps;
l.payoutWallets.push(creator);
l.payoutWeights.push(10000);
poolManager.unlock(abi.encode(Action.Lock, token));
// Burn any launch-token dust the position did not absorb (mirrors V3's burn-leftover; keeps the
// locker off terminals as a holder). ETH is never left here from a lock (single-sided token).
uint256 dust = IERC20(token).balanceOf(address(this));
if (dust > 0) IERC20(token).safeTransfer(DEAD, dust);
emit PositionLocked(token, liquidity, creator);
}
// ─────────────────────────── fee collection ───────────────────────────
/// @notice Permissionless. Pokes `token`'s locked position to realize its accrued LP-tier fees, then
/// credits them (native ETH + the launch token) to the creator's payout wallets and the treasury per
/// the immutable 50/50 split. Recipients later pull via withdraw*.
function collectFees(address token) external nonReentrant returns (uint256 tokenAmount, uint256 ethAmount) {
Lock storage l = _locks[token];
if (!l.exists) revert UnknownToken();
bytes memory res = poolManager.unlock(abi.encode(Action.Collect, token));
(ethAmount, tokenAmount) = abi.decode(res, (uint256, uint256)); // (currency0=ETH, currency1=token)
// Creator share of THIS collection: the hook's tracked MC-weighted split (variable) when it
// exposes pendingSplitBps, else the fixed creatorSplitBps set at lock time (back-compat).
uint16 splitBps = _splitBps(l);
_distribute(l, splitBps, token, tokenAmount, false);
_distribute(l, splitBps, token, ethAmount, true);
emit FeesCollected(token, tokenAmount, ethAmount);
}
/// @dev The creator's bps share for a collection: prefer the hook's tracked variable split
/// (pendingSplitBps), fall back to the fixed creatorSplitBps on any read failure. staticcall so a
/// hook without the view (or that reverts) can never brick fee collection.
function _splitBps(Lock storage l) internal view returns (uint16) {
(bool ok, bytes memory ret) = address(l.key.hooks).staticcall(
abi.encodeWithSignature("pendingSplitBps(bytes32)", PoolId.unwrap(l.key.toId()))
);
if (ok && ret.length >= 32) {
uint256 v = abi.decode(ret, (uint256));
if (v <= 10000) return uint16(v);
}
return l.creatorSplitBps;
}
// ─────────────────────────── graduation (re-range in place) ───────────────────────────
/// @notice Factory-only, exactly-once. Re-ranges the locked single-sided CURVE position into a
/// full-range [newTickLower, newTickUpper] deep position at the current pool price. The liquidity
/// NEVER leaves the locker's custody — it is removed from the curve range and immediately re-added
/// full-range in one unlock, so the "locked forever" guarantee is preserved (there is still no path
/// that withdraws liquidity to anyone). Any side-dust the full-range add cannot absorb is burned
/// (token → DEAD) / swept (ETH → treasury), mirroring the V3 graduation. Returns the amounts placed.
function graduateRerange(address token, int24 newTickLower, int24 newTickUpper)
external
nonReentrant
returns (uint256 ethToLp, uint256 tokensToLp)
{
if (msg.sender != factory) revert NotFactory();
Lock storage l = _locks[token];
if (!l.exists) revert UnknownToken();
if (l.graduated) revert AlreadyGraduated();
l.graduated = true; // effect-first: exactly-once
bytes memory res = poolManager.unlock(abi.encode(Action.Graduate, token, newTickLower, newTickUpper));
uint128 newLiq;
(ethToLp, tokensToLp, newLiq) = abi.decode(res, (uint256, uint256, uint128));
l.tickLower = newTickLower;
l.tickUpper = newTickUpper;
l.liquidity = newLiq;
emit PositionGraduated(token, newLiq, ethToLp, tokensToLp);
}
/// @notice PoolManager unlock callback. Dispatches the Lock (add liquidity), Collect (poke fees) and
/// Graduate (re-range) actions. Only ever entered via this contract's own `unlock` calls.
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert NotPoolManager();
Action action = abi.decode(data, (Action));
if (action == Action.Graduate) return _graduateCallback(data);
(, address token) = abi.decode(data, (Action, address));
Lock storage l = _locks[token];
if (action == Action.Lock) {
(BalanceDelta delta,) = poolManager.modifyLiquidity(
l.key,
ModifyLiquidityParams({
tickLower: l.tickLower,
tickUpper: l.tickUpper,
liquidityDelta: int256(uint256(l.liquidity)),
salt: 0
}),
""
);
// Single-sided token1 position at current tick == tickUpper: only currency1 (token) is owed.
// Settle whatever each side owes (defensive; currency0 owed is expected to be 0).
if (delta.amount0() < 0) {
l.key.currency0.settle(poolManager, address(this), uint256(uint128(-delta.amount0())), false);
}
if (delta.amount1() < 0) {
l.key.currency1.settle(poolManager, address(this), uint256(uint128(-delta.amount1())), false);
}
return "";
}
// Collect: liquidityDelta 0 pokes the position; callerDelta == fees owed (>= 0). Take both sides.
(BalanceDelta fees,) = poolManager.modifyLiquidity(
l.key,
ModifyLiquidityParams({
tickLower: l.tickLower,
tickUpper: l.tickUpper,
liquidityDelta: 0,
salt: 0
}),
""
);
uint256 eth0 = fees.amount0() > 0 ? uint256(uint128(fees.amount0())) : 0;
uint256 tok1 = fees.amount1() > 0 ? uint256(uint128(fees.amount1())) : 0;
if (eth0 > 0) poolManager.take(l.key.currency0, address(this), eth0); // native ETH to locker
if (tok1 > 0) poolManager.take(l.key.currency1, address(this), tok1);
return abi.encode(eth0, tok1);
}
/// @dev Graduate action: remove the whole curve position and re-add it full-range at the current
/// price, in one unlock. Never withdraws to any external party — the freed ETH+token go straight back
/// into the new locked position; only the unavoidable single-side dust is swept (ETH → treasury) /
/// burned (token → DEAD). Returns (ethPlaced, tokenPlaced, newLiquidity).
function _graduateCallback(bytes calldata data) internal returns (bytes memory) {
(, address token, int24 newLower, int24 newUpper) = abi.decode(data, (Action, address, int24, int24));
Lock storage l = _locks[token];
// 1. Remove ALL curve liquidity (also realizes accrued LP fees) → positive deltas owed to us.
BalanceDelta rm = _modify(l.key, l.tickLower, l.tickUpper, -int256(uint256(l.liquidity)));
// 2. Size + add a full-range position from the freed amounts at the live price. getLiquidityFor-
// Amounts guarantees the add never needs more than `freed` on either side (net residual >= 0).
uint128 newLiq = _sizeFullRange(l.key, newLower, newUpper, rm);
BalanceDelta add = _modify(l.key, newLower, newUpper, int256(uint256(newLiq)));
// 3. Zero the net deltas: take the residual dust and dispose of it (ETH → treasury real value;
// token → DEAD so the locker is never a phantom holder). Touches ONLY this graduation's dust —
// never the locker's held withdrawal balances.
_sweepResidual(l, rm, add);
return abi.encode(_neg0(add), _neg1(add), newLiq);
}
function _modify(PoolKey storage key, int24 tickLower, int24 tickUpper, int256 liquidityDelta)
internal
returns (BalanceDelta delta)
{
(delta,) = poolManager.modifyLiquidity(
key, ModifyLiquidityParams({tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: liquidityDelta, salt: 0}), ""
);
}
function _sizeFullRange(PoolKey storage key, int24 newLower, int24 newUpper, BalanceDelta rm)
internal
view
returns (uint128)
{
(uint160 sqrtP,,,) = poolManager.getSlot0(key.toId());
uint256 ethFreed = rm.amount0() > 0 ? uint256(uint128(rm.amount0())) : 0;
uint256 tokFreed = rm.amount1() > 0 ? uint256(uint128(rm.amount1())) : 0;
return LiquidityAmounts.getLiquidityForAmounts(
sqrtP, TickMath.getSqrtPriceAtTick(newLower), TickMath.getSqrtPriceAtTick(newUpper), ethFreed, tokFreed
);
}
function _sweepResidual(Lock storage l, BalanceDelta rm, BalanceDelta add) internal {
int128 net0 = rm.amount0() + add.amount0();
int128 net1 = rm.amount1() + add.amount1();
if (net0 > 0) poolManager.take(l.key.currency0, l.treasury, uint256(uint128(net0)));
if (net1 > 0) poolManager.take(l.key.currency1, DEAD, uint256(uint128(net1)));
}
function _neg0(BalanceDelta d) internal pure returns (uint256) {
return d.amount0() < 0 ? uint256(uint128(-d.amount0())) : 0;
}
function _neg1(BalanceDelta d) internal pure returns (uint256) {
return d.amount1() < 0 ? uint256(uint128(-d.amount1())) : 0;
}
/// @dev Split `amount` of one asset (ETH if `isEth`, else the launch `token`) 50/50 (per
/// creatorSplitBps) between the creator's payout wallets and the treasury. All rounding dust accrues
/// to the treasury, so credited amounts sum EXACTLY to `amount` (identical to the V3 FeeLocker).
function _distribute(Lock storage l, uint16 splitBps, address token, uint256 amount, bool isEth) internal {
if (amount == 0) return;
uint256 creatorShare = (amount * splitBps) / 10000;
uint256 protocolShare = amount - creatorShare;
uint256 distributed;
uint256 n = l.payoutWallets.length;
for (uint256 i = 0; i < n; i++) {
uint256 cut = (creatorShare * l.payoutWeights[i]) / 10000;
if (cut > 0) _credit(l.payoutWallets[i], token, cut, isEth);
distributed += cut;
}
uint256 toTreasury = protocolShare + (creatorShare - distributed);
if (toTreasury > 0) _credit(l.treasury, token, toTreasury, isEth);
}
function _credit(address to, address token, uint256 amount, bool isEth) internal {
if (isEth) {
withdrawableETH[to] += amount;
} else {
withdrawableToken[token][to] += amount;
}
}
// ─────────────────────────── withdrawals (pull) ───────────────────────────
function withdrawETH() external nonReentrant returns (uint256 amount) {
amount = withdrawableETH[msg.sender];
if (amount == 0) revert NothingToWithdraw();
withdrawableETH[msg.sender] = 0;
(bool ok,) = msg.sender.call{value: amount}("");
require(ok, "ETH transfer failed");
emit ETHWithdrawn(msg.sender, amount);
}
function withdrawToken(address token) external nonReentrant returns (uint256 amount) {
amount = withdrawableToken[token][msg.sender];
if (amount == 0) revert NothingToWithdraw();
withdrawableToken[token][msg.sender] = 0;
IERC20(token).safeTransfer(msg.sender, amount);
emit TokenWithdrawn(token, msg.sender, amount);
}
// ─────────────────────────── creator role + payout config ───────────────────────────
function transferCreator(address token, address newCreator) external {
Lock storage l = _locks[token];
if (!l.exists) revert UnknownToken();
if (msg.sender != l.creator) revert NotCreator();
if (newCreator == address(0)) revert BadPayoutConfig();
address old = l.creator;
l.creator = newCreator;
_resetPayout(l, newCreator);
emit CreatorTransferred(token, old, newCreator);
}
/// @notice Community takeover: the protocol treasury (multisig) reassigns the creator role for an
/// abandoned coin. Redirects only FUTURE creator fees; already-credited balances are untouchable.
function communityTakeover(address token, address newCreator) external {
Lock storage l = _locks[token];
if (!l.exists) revert UnknownToken();
if (msg.sender != l.treasury) revert NotTreasury();
if (newCreator == address(0)) revert BadPayoutConfig();
address old = l.creator;
l.creator = newCreator;
_resetPayout(l, newCreator);
emit CommunityTakeover(token, old, newCreator);
}
function setPayoutConfig(address token, address[] calldata wallets, uint16[] calldata weights) external {
Lock storage l = _locks[token];
if (!l.exists) revert UnknownToken();
if (msg.sender != l.creator) revert NotCreator();
_validateAndSet(l, wallets, weights);
emit PayoutConfigSet(token, wallets, weights);
}
function _validateAndSet(Lock storage l, address[] calldata wallets, uint16[] calldata weights) internal {
uint256 n = wallets.length;
if (n == 0 || n > MAX_PAYOUT_WALLETS || weights.length != n) revert BadPayoutConfig();
uint256 sum;
for (uint256 i = 0; i < n; i++) {
if (wallets[i] == address(0)) revert BadPayoutConfig();
sum += weights[i];
}
if (sum != 10000) revert BadPayoutConfig();
delete l.payoutWallets;
delete l.payoutWeights;
for (uint256 i = 0; i < n; i++) {
l.payoutWallets.push(wallets[i]);
l.payoutWeights.push(weights[i]);
}
}
function _resetPayout(Lock storage l, address newCreator) internal {
delete l.payoutWallets;
delete l.payoutWeights;
l.payoutWallets.push(newCreator);
l.payoutWeights.push(10000);
}
// ─────────────────────────── views ───────────────────────────
function getLock(address token)
external
view
returns (
bool exists,
uint128 liquidity,
address creator,
address treasury,
uint16 creatorSplitBps,
address[] memory payoutWallets,
uint16[] memory payoutWeights
)
{
Lock storage l = _locks[token];
return
(l.exists, l.liquidity, l.creator, l.treasury, l.creatorSplitBps, l.payoutWallets, l.payoutWeights);
}
function liquidityOf(address token) external view returns (uint128) {
return _locks[token].liquidity;
}
/// @notice The lock's current tick range (curve range pre-graduation, full range after).
function lockTicks(address token) external view returns (int24 tickLower, int24 tickUpper) {
Lock storage l = _locks[token];
return (l.tickLower, l.tickUpper);
}
/// @notice Whether the position has been re-ranged to full-range (graduated).
function isGraduated(address token) external view returns (bool) {
return _locks[token].graduated;
}
}
Chain explorer2712msChain node79ms