FeatherFactory
0x106600e8fa3c688df23ab3e70a0c9c85c45927f6
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
11,028 bytes
ABI entries
39
22 read · 4 write
License
none
Contract information
- Address
- 0x106600e8fa3c688df23ab3e70a0c9c85c45927f6
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x65B9FCCC5F…1DF3c22feC
- Creation tx
- 0xe5dc43e2e1…fec62a670e
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (22)
CURVE_SUPPLY() → uint256
CURVE_TICK_WIDTH() → int24
MIN_TICK() → int24
RESERVE_SUPPLY() → uint256
TICK_SPACING() → int24
TOTAL_SUPPLY() → uint256
bandPlan() → uint128, uint256, uint128, uint256
gradTick() → int24
hook() → address
launchNonce(address) → uint256
launchTick() → int24
locker() → address
owner() → address
paused() → bool
permit2() → address
poolKeyFor(address) → tuple
poolManager() → address
positionManager() → address
predictTokenAddress(address, string, uint256) → address
predictVanityTokenAddress(bytes32) → address
registry() → address
tokenInitCodeHash() → bytes32
Events (3)
LaunchedOwnershipTransferredPausedSet
ABI
[
{
"inputs": [
{
"internalType": "contract FeatherRegistry",
"name": "registry_",
"type": "address"
},
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "contract IPositionManager",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "contract IAllowanceTransfer",
"name": "permit2_",
"type": "address"
},
{
"internalType": "address",
"name": "hook_",
"type": "address"
},
{
"internalType": "address",
"name": "locker_",
"type": "address"
},
{
"internalType": "int24",
"name": "launchTick_",
"type": "int24"
},
{
"internalType": "address",
"name": "owner_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "InvalidLaunchTick",
"type": "error"
},
{
"inputs": [],
"name": "LaunchesPaused",
"type": "error"
},
{
"inputs": [],
"name": "NativeTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "NotOwner",
"type": "error"
},
{
"inputs": [],
"name": "NotPoolManager",
"type": "error"
},
{
"inputs": [],
"name": "SaltNotOwned",
"type": "error"
},
{
"inputs": [],
"name": "SlippageExceeded",
"type": "error"
},
{
"inputs": [],
"name": "TokenTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethSpentOnBuy",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensBought",
"type": "uint256"
}
],
"name": "Launched",
"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": false,
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "PausedSet",
"type": "event"
},
{
"inputs": [],
"name": "CURVE_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CURVE_TICK_WIDTH",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MIN_TICK",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "RESERVE_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"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": [],
"name": "bandPlan",
"outputs": [
{
"internalType": "uint128",
"name": "curveLiquidity",
"type": "uint128"
},
{
"internalType": "uint256",
"name": "curveAmount",
"type": "uint256"
},
{
"internalType": "uint128",
"name": "reserveLiquidity",
"type": "uint128"
},
{
"internalType": "uint256",
"name": "reserveTarget",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "gradTick",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "uint256",
"name": "initialBuyMinOut",
"type": "uint256"
},
{
"internalType": "bytes32",
"name": "vanitySalt",
"type": "bytes32"
}
],
"name": "launch",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "launchNonce",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "launchTick",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "locker",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "paused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "permit2",
"outputs": [
{
"internalType": "contract IAllowanceTransfer",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "poolKeyFor",
"outputs": [
{
"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": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "contract IPositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
}
],
"name": "predictTokenAddress",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "vanitySalt",
"type": "bytes32"
}
],
"name": "predictVanityTokenAddress",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "registry",
"outputs": [
{
"internalType": "contract FeatherRegistry",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "paused_",
"type": "bool"
}
],
"name": "setPaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "tokenInitCodeHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {LiquidityAmounts} from "@uniswap/v4-periphery/src/libraries/LiquidityAmounts.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
import {FeatherToken} from "./FeatherToken.sol";
import {FeatherRegistry} from "./FeatherRegistry.sol";
import {FeatherFeeHook} from "./FeatherFeeHook.sol";
import {LpLocker} from "./LpLocker.sol";
/// @title FeatherFactory
/// @notice Single entry point for launching a feather token; a versioned
/// writer behind the permanent FeatherRegistry.
///
/// `launch()` performs the entire lifecycle in one transaction:
/// 1. deploys the ERC-20 as a full contract via CREATE2 (no proxy;
/// salt is keccak(creator, symbol, nonce), or a caller-mined
/// vanity salt whose first 20 bytes must equal the caller),
/// 2. mints the fixed 1B supply to itself,
/// 3. initializes the canonical Uniswap v4 pool (token / native
/// ETH, dynamic-fee flag, FeatherFeeHook) at the launch tick,
/// 4. registers the launch, emitting `TokenLaunched` from the
/// registry's permanent address,
/// 5. mints the supply as two single-sided positions — the curve
/// band (79.31%, pump.fun's bonding curve tick-for-tick) and
/// the reserve band (20.69%, pump's post-graduation deposit) —
/// owned by the LpLocker from the moment they exist,
/// 6. executes the creator's optional initial buy as an ordinary,
/// unprivileged swap that pays the standard hook fee.
///
/// Curve constants replicate pump.fun's dollar optics ($69,000
/// graduation market cap, x14.7 from launch), frozen in ETH terms at
/// the deploy-time ETH/USD rate. The launch tick is immutable deploy
/// config; band liquidity is derived on-chain from the fixed band
/// amounts so supply conservation is exact.
///
/// The pause switch affects new launches only. The factory retains
/// no authority over anything it has deployed.
///
/// @dev Native ETH is currency0 and v4 prices are quoted currency1/currency0
/// (token per ETH), so the token's ETH price rises as the tick falls.
/// The curve band spans [launchTick - 26880, launchTick]; graduation is
/// the tick reaching gradTick = launchTick - 26880. 26880 ticks =
/// ln(14.694)/ln(1.0001) rounded to tick spacing 60, pump's x14.69
/// price multiple.
contract FeatherFactory is IUnlockCallback {
// ---------------------------------------------------------------------
// Supply and curve shape (pump.fun replica)
// ---------------------------------------------------------------------
uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
uint256 public constant CURVE_SUPPLY = 793_100_000e18; // 79.31%
uint256 public constant RESERVE_SUPPLY = 206_900_000e18; // 20.69%
/// @dev ln(14.694)/ln(1.0001) ~= 26877, rounded to tick spacing 60.
int24 public constant CURVE_TICK_WIDTH = 26880;
int24 public constant TICK_SPACING = 60;
/// @dev TickMath.minUsableTick(60): the reserve band's lower bound.
int24 public constant MIN_TICK = -887220;
// ---------------------------------------------------------------------
// Immutable deploy config
// ---------------------------------------------------------------------
FeatherRegistry public immutable registry;
IPoolManager public immutable poolManager;
IPositionManager public immutable positionManager;
IAllowanceTransfer public immutable permit2;
address public immutable hook;
address public immutable locker;
/// @notice Pool tick at launch, fixed at deploy from the ETH/USD anchor
/// so that launch market cap is ~$4,696 and graduation market
/// cap is ~$69,000 at that rate.
int24 public immutable launchTick;
/// @notice Tick at which the curve band is fully sold — the graduation
/// condition. The hook latches a permanent per-token `graduated`
/// flag at the first crossing; there is no migration or trading
/// pause, the pool simply opens to external liquidity.
int24 public immutable gradTick;
// ---------------------------------------------------------------------
// State
// ---------------------------------------------------------------------
address public owner;
bool public paused;
/// @notice Per-creator launch nonce (CREATE2 salt component).
mapping(address => uint256) public launchNonce;
// ---------------------------------------------------------------------
// Events / errors
// ---------------------------------------------------------------------
event Launched(
address indexed token,
bytes32 indexed poolId,
address indexed creator,
uint256 ethSpentOnBuy,
uint256 tokensBought
);
event PausedSet(bool paused);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
error NotOwner();
error LaunchesPaused();
error SlippageExceeded();
error NotPoolManager();
error InvalidLaunchTick();
error ZeroAddress();
error NativeTransferFailed();
error TokenTransferFailed();
error SaltNotOwned();
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
constructor(
FeatherRegistry registry_,
IPoolManager poolManager_,
IPositionManager positionManager_,
IAllowanceTransfer permit2_,
address hook_,
address locker_,
int24 launchTick_,
address owner_
) {
registry = registry_;
poolManager = poolManager_;
positionManager = positionManager_;
permit2 = permit2_;
hook = hook_;
locker = locker_;
owner = owner_;
if (launchTick_ % TICK_SPACING != 0) revert InvalidLaunchTick();
if (launchTick_ - CURVE_TICK_WIDTH <= MIN_TICK) revert InvalidLaunchTick();
if (launchTick_ >= TickMath.maxUsableTick(TICK_SPACING)) revert InvalidLaunchTick();
launchTick = launchTick_;
gradTick = launchTick_ - CURVE_TICK_WIDTH;
emit OwnershipTransferred(address(0), owner_);
}
// ---------------------------------------------------------------------
// Launch
// ---------------------------------------------------------------------
/// @notice Launches a feather token. There is no launch fee: the full
/// `msg.value` funds the creator's initial buy through the pool,
/// subject to `initialBuyMinOut`; zero `msg.value` is a valid
/// free launch.
/// @param vanitySalt Optional CREATE2 salt mined off-chain for a vanity
/// token address (see `predictVanityTokenAddress`). Its first 20
/// bytes must equal the caller's address, so a mined salt is
/// spendable only by the wallet it was mined for and cannot be
/// sniped if observed. `bytes32(0)` selects the default
/// nonce-derived salt.
function launch(
string calldata name,
string calldata symbol,
string calldata metadataURI,
uint256 initialBuyMinOut,
bytes32 vanitySalt
) external payable returns (address token, bytes32 poolId) {
if (paused) revert LaunchesPaused();
bytes32 salt;
if (vanitySalt == bytes32(0)) {
salt = keccak256(abi.encode(msg.sender, symbol, launchNonce[msg.sender]++));
} else {
// Truncation is intentional: a vanity salt's first 20 bytes
// encode the launcher address by construction.
// forge-lint: disable-next-line(unsafe-typecast)
if (address(bytes20(vanitySalt)) != msg.sender) revert SaltNotOwned();
salt = vanitySalt;
}
// Full CREATE2 token contract (no proxy); the fixed supply is minted
// to the factory within the same transaction.
token = address(new FeatherToken{salt: salt}());
FeatherToken(token).initialize(name, symbol, address(this), TOTAL_SUPPLY);
{
// Canonical pool: token vs native ETH, dynamic-fee flag, hook.
PoolKey memory key = poolKeyFor(token);
poolManager.initialize(key, TickMath.getSqrtPriceAtTick(launchTick));
poolId = PoolId.unwrap(key.toId());
// Registration precedes the band mints: the locker verifies the
// positions against registry.canonicalPool(token), and
// TokenLaunched then precedes every other event of the launch.
registry.register(token, poolId, msg.sender, name, symbol, metadataURI);
// Curve + reserve bands, owned by the LpLocker from the mint.
_mintLockedBands(key, token);
}
// Creator initial buy: same pool, same hook fee as any other trader.
uint256 tokensBought = _initialBuy(poolKeyFor(token), msg.value, initialBuyMinOut);
emit Launched(token, poolId, msg.sender, msg.value, tokensBought);
}
function _initialBuy(PoolKey memory key, uint256 buyEth, uint256 minOut) internal returns (uint256 tokensBought) {
if (buyEth == 0) {
if (minOut > 0) revert SlippageExceeded();
return 0;
}
bytes memory result = poolManager.unlock(abi.encode(key, buyEth, minOut, msg.sender));
uint256 ethSpent;
(tokensBought, ethSpent) = abi.decode(result, (uint256, uint256));
// Refund any ETH the swap did not consume so nothing is stranded in
// the factory. Only reachable for zero-fee dust fills: the hook's
// fill guard reverts any fee-bearing partial fill.
if (ethSpent < buyEth) {
(bool ok,) = msg.sender.call{value: buyEth - ethSpent}("");
if (!ok) revert NativeTransferFailed();
}
}
/// @dev Initial-buy swap, executed inside the PoolManager unlock — an
/// ordinary unprivileged swap that walks the curve band and pays
/// the hook fee like any later buyer.
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert NotPoolManager();
(PoolKey memory key, uint256 ethIn, uint256 minOut, address recipient) =
abi.decode(data, (PoolKey, uint256, uint256, address));
BalanceDelta delta = poolManager.swap(
key,
SwapParams({
// Casting is safe: ethIn is bounded by msg.value, far below
// type(int256).max.
// forge-lint: disable-next-line(unsafe-typecast)
zeroForOne: true,
// forge-lint: disable-next-line(unsafe-typecast)
amountSpecified: -int256(ethIn),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
""
);
uint256 ethOwed = uint256(uint128(uint256(-int256(delta.amount0()))));
poolManager.settle{value: ethOwed}();
uint256 tokensOut = uint256(uint128(uint256(int256(delta.amount1()))));
if (tokensOut < minOut) revert SlippageExceeded();
poolManager.take(key.currency1, recipient, tokensOut);
return abi.encode(tokensOut, ethOwed);
}
// ---------------------------------------------------------------------
// Views
// ---------------------------------------------------------------------
/// @notice The canonical PoolKey for a feather token.
function poolKeyFor(address token) public view returns (PoolKey memory) {
return PoolKey({
currency0: CurrencyLibrary.ADDRESS_ZERO,
currency1: Currency.wrap(token),
fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
tickSpacing: TICK_SPACING,
hooks: IHooks(hook)
});
}
/// @notice keccak256 of FeatherToken's creation code — the CREATE2
/// init-code hash used by off-chain vanity miners and address
/// verifiers. Constant, as FeatherToken takes no constructor
/// arguments.
function tokenInitCodeHash() public pure returns (bytes32) {
return keccak256(type(FeatherToken).creationCode);
}
/// @notice Predicts the token address of a default (nonce-salted)
/// launch, so anyone can verify a token was factory-made from
/// its address alone.
function predictTokenAddress(address creator, string calldata symbol, uint256 nonce)
external
view
returns (address)
{
return _create2Address(keccak256(abi.encode(creator, symbol, nonce)));
}
/// @notice Predicts the token address of a vanity-salted launch. The
/// salt's first 20 bytes encode the launcher (enforced in
/// `launch`), so no separate creator input is needed.
function predictVanityTokenAddress(bytes32 vanitySalt) external view returns (address) {
return _create2Address(vanitySalt);
}
function _create2Address(bytes32 salt) internal view returns (address) {
return
address(
uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, tokenInitCodeHash()))))
);
}
/// @notice Band liquidities as the factory will mint them.
function bandPlan()
public
view
returns (uint128 curveLiquidity, uint256 curveAmount, uint128 reserveLiquidity, uint256 reserveTarget)
{
uint160 sqrtLaunch = TickMath.getSqrtPriceAtTick(launchTick);
uint160 sqrtGrad = TickMath.getSqrtPriceAtTick(gradTick);
uint160 sqrtMin = TickMath.getSqrtPriceAtTick(MIN_TICK);
curveLiquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtGrad, sqrtLaunch, CURVE_SUPPLY);
curveAmount = SqrtPriceMath.getAmount1Delta(sqrtGrad, sqrtLaunch, curveLiquidity, true);
// The reserve band absorbs the curve band's tick-rounding remainder,
// so the two bands always sum to the full supply; sub-token residue
// is locked in the LpLocker.
reserveTarget = TOTAL_SUPPLY - curveAmount;
reserveLiquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtMin, sqrtGrad, reserveTarget);
}
// ---------------------------------------------------------------------
// Owner ops — new launches only
// ---------------------------------------------------------------------
function setPaused(bool paused_) external onlyOwner {
paused = paused_;
emit PausedSet(paused_);
}
function transferOwnership(address newOwner) external onlyOwner {
if (newOwner == address(0)) revert ZeroAddress();
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
// ---------------------------------------------------------------------
// Internals
// ---------------------------------------------------------------------
function _mintLockedBands(PoolKey memory key, address token) internal {
(uint128 curveLiquidity,, uint128 reserveLiquidity,) = bandPlan();
// The two MINT_POSITIONs below take the next two sequential ids.
uint256 curvePositionId = positionManager.nextTokenId();
// The PositionManager settles ERC-20s through Permit2.
FeatherToken(token).approve(address(permit2), type(uint256).max);
permit2.approve(token, address(positionManager), type(uint160).max, type(uint48).max);
bytes memory actions =
abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.MINT_POSITION), uint8(Actions.CLOSE_CURRENCY));
bytes[] memory params = new bytes[](3);
// Curve band [gradTick, launchTick]: single-sided in the token at
// the launch price — pump's bonding curve tick-for-tick.
params[0] =
abi.encode(key, gradTick, launchTick, uint256(curveLiquidity), 0, type(uint128).max, locker, bytes(""));
// Reserve band [MIN_TICK, gradTick]: activates when the price
// crosses graduation, mirroring pump's post-graduation deposit.
params[1] =
abi.encode(key, MIN_TICK, gradTick, uint256(reserveLiquidity), 0, type(uint128).max, locker, bytes(""));
params[2] = abi.encode(key.currency1);
// The hook rejects every pre-graduation liquidity addition except
// these two mints: this pool's gate is open only between the calls
// below, inside the atomic launch.
FeatherFeeHook(payable(hook)).openLiquidityGate(token);
positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp);
FeatherFeeHook(payable(hook)).closeLiquidityGate(token);
// Record the positions so the locker's permissionless fees-only
// sweep (`collectFees`) can find them.
LpLocker(locker).registerPositions(token, curvePositionId, curvePositionId + 1);
// Sub-token dust from liquidity rounding is locked with the LP.
uint256 dust = FeatherToken(token).balanceOf(address(this));
if (dust > 0 && !FeatherToken(token).transfer(locker, dust)) revert TokenTransferFailed();
}
}
Chain explorer3680msChain node88ms