StockFunLauncher
0x3ebcc8c52d05277e8f2a5a3282e847ca0936f23d
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
16,314 bytes
ABI entries
33
14 read · 8 write
License
none
Contract information
- Address
- 0x3ebcc8c52d05277e8f2a5a3282e847ca0936f23d
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x8818E32262…9459ACd686
- Creation tx
- 0xa63d79f2c5…9b36ccd6b8
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (14)
BPS() → uint16
FEE_TIER() → uint24
TICK_SPACING() → int24
TOTAL_SUPPLY() → uint256
TRANCHES() → uint256
feeRecipient() → address
getLaunch(uint256) → tuple
launchFee() → uint256
launchesCount() → uint256
owner() → address
permit2() → address
poolManager() → address
positionManager() → address
rescueEnabled() → bool
Events (7)
FeeRecipientSetFeesCollectedLaunchFeeSetLaunchedOwnershipTransferredRescueDisabledRescued
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "contract IPositionManager",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "contract IAllowanceTransfer",
"name": "permit2_",
"type": "address"
},
{
"internalType": "address",
"name": "owner_",
"type": "address"
},
{
"internalType": "uint256",
"name": "launchFee_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "InvalidLaunchFee",
"type": "error"
},
{
"inputs": [],
"name": "InvalidRange",
"type": "error"
},
{
"inputs": [],
"name": "RescueIsDisabled",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "FeeRecipientSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "launchId",
"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": false,
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"name": "LaunchFeeSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "launchId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "stockToken",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "firstPositionTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint24",
"name": "fee",
"type": "uint24"
}
],
"name": "Launched",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "RescueDisabled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "launchId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "positionTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "Rescued",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FEE_TIER",
"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": [],
"name": "TRANCHES",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "launchId",
"type": "uint256"
}
],
"name": "collectFees",
"outputs": [
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "disableRescue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "feeRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "launchId",
"type": "uint256"
}
],
"name": "getLaunch",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "stockToken",
"type": "address"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "uint256",
"name": "firstPositionTokenId",
"type": "uint256"
},
{
"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": "poolKey",
"type": "tuple"
},
{
"internalType": "int24[5]",
"name": "tickBounds",
"type": "int24[5]"
}
],
"internalType": "struct StockFunLauncher.Launch",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "address",
"name": "stockToken",
"type": "address"
},
{
"internalType": "int24",
"name": "startingTick",
"type": "int24"
}
],
"internalType": "struct StockFunLauncher.LaunchParams",
"name": "p",
"type": "tuple"
}
],
"name": "launch",
"outputs": [
{
"internalType": "uint256",
"name": "launchId",
"type": "uint256"
},
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "launchFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "launchesCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "permit2",
"outputs": [
{
"internalType": "contract IAllowanceTransfer",
"name": "",
"type": "address"
}
],
"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": "uint256",
"name": "launchId",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "rescue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "rescueEnabled",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "setFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"name": "setLaunchFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdrawETH",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Owned} from "solmate/src/auth/Owned.sol";
import {ERC20} from "solmate/src/tokens/ERC20.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "v4-periphery/src/libraries/Actions.sol";
import {LiquidityAmounts} from "v4-periphery/src/libraries/LiquidityAmounts.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {StockFunToken} from "./StockFunToken.sol";
/// @notice stock.fun launcher — launches a fixed-supply token one-sided into a
/// hookless Uniswap v4 pool paired against a Robinhood Chain stock token.
///
/// Liquidity ladder: the full supply is placed as FOUR one-sided ranges above
/// the starting price so the market-cap curve follows the designed checkpoints
/// (calibrated for a ~$7k starting mcap: ~$9k of net buys per checkpoint
/// segment → $50k / $150k / $250k, with a deep tail above).
///
/// tranche price multiple of start supply
/// 1 1x → ~7.10x 47.65%
/// 2 7.10x → ~21.3x 10.32%
/// 3 21.3x → ~35.9x 4.59%
/// 4 (tail) 35.9x → max tick 37.44%
///
/// LP custody: all four positions are PositionManager ERC-721s held by this
/// contract. No public path removes liquidity. Owner-only `rescue` is a
/// testing-phase escape hatch; `disableRescue()` burns it permanently.
/// No creator fees: LP fees are collectable only to the protocol `feeRecipient`.
contract StockFunLauncher is Owned {
uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
uint256 public constant TRANCHES = 4;
uint16 public constant BPS = 10_000;
/// @dev Supply share per tranche, bps. Sums to 10_000.
uint16 internal constant BPS_T1 = 4_765;
uint16 internal constant BPS_T2 = 1_032;
uint16 internal constant BPS_T3 = 459;
uint16 internal constant BPS_T4 = 3_744;
/// @dev Tranche upper bounds as tick offsets from the aligned start tick
/// (multiples of the 1% tier's 200 spacing; ≈7.10x / 21.3x / 35.9x price).
int24 internal constant OFFSET_T1 = 19_600;
int24 internal constant OFFSET_T2 = 30_600;
int24 internal constant OFFSET_T3 = 35_800;
uint24 public constant FEE_TIER = 10_000; // 1%
int24 public constant TICK_SPACING = 200;
IPoolManager public immutable poolManager;
IPositionManager public immutable positionManager;
IAllowanceTransfer public immutable permit2;
/// @notice Flat ETH fee charged per launch, retained in this contract until withdrawn.
uint256 public launchFee;
/// @notice Receiver of collected LP fees.
address public feeRecipient;
/// @notice Testing-phase escape hatch switch; irreversibly disabled via disableRescue().
bool public rescueEnabled = true;
struct Launch {
address token;
address stockToken;
address creator;
/// First of TRANCHES consecutive PositionManager token ids.
uint256 firstPositionTokenId;
PoolKey poolKey;
/// Tranche boundary ticks, start-side first (ascending when the token
/// is currency0, descending when it is currency1).
int24[5] tickBounds;
}
Launch[] internal _launches;
struct LaunchParams {
string name;
string symbol;
address stockToken;
/// Starting price expressed as a tick of stock-per-token: price = 1.0001^startingTick.
/// Orientation is handled internally regardless of currency sort order.
int24 startingTick;
}
event Launched(
uint256 indexed launchId,
address indexed token,
address indexed stockToken,
address creator,
uint256 firstPositionTokenId,
uint24 fee
);
event FeesCollected(uint256 indexed launchId, uint256 amount0, uint256 amount1);
event Rescued(uint256 indexed launchId, uint256 positionTokenId, address to);
event RescueDisabled();
event LaunchFeeSet(uint256 fee);
event FeeRecipientSet(address recipient);
error InvalidLaunchFee();
error InvalidRange();
error RescueIsDisabled();
constructor(
IPoolManager poolManager_,
IPositionManager positionManager_,
IAllowanceTransfer permit2_,
address owner_,
uint256 launchFee_
) Owned(owner_) {
poolManager = poolManager_;
positionManager = positionManager_;
permit2 = permit2_;
launchFee = launchFee_;
feeRecipient = owner_;
}
// ---------------------------------------------------------------- launch
function launch(LaunchParams calldata p) external payable returns (uint256 launchId, address token) {
if (msg.value != launchFee) revert InvalidLaunchFee();
StockFunToken t = new StockFunToken(p.name, p.symbol, TOTAL_SUPPLY, address(this));
token = address(t);
bool tokenIs0 = token < p.stockToken;
PoolKey memory key = PoolKey({
currency0: Currency.wrap(tokenIs0 ? token : p.stockToken),
currency1: Currency.wrap(tokenIs0 ? p.stockToken : token),
fee: FEE_TIER,
tickSpacing: TICK_SPACING,
hooks: IHooks(address(0))
});
// Pool tick is stock-per-token when the token is currency0, inverted otherwise.
int24 poolTick = tokenIs0 ? p.startingTick : -p.startingTick;
poolManager.initialize(key, TickMath.getSqrtPriceAtTick(poolTick));
int24[5] memory bounds = _trancheBounds(poolTick, tokenIs0);
uint256 firstPositionTokenId = _mintTranches(t, key, bounds, tokenIs0);
// Rounding dust from the bps split stays out of the pool; send it to the creator.
uint256 dust = t.balanceOf(address(this));
if (dust > 0) t.transfer(msg.sender, dust);
launchId = _launches.length;
_launches.push(
Launch({
token: token,
stockToken: p.stockToken,
creator: msg.sender,
firstPositionTokenId: firstPositionTokenId,
poolKey: key,
tickBounds: bounds
})
);
emit Launched(launchId, token, p.stockToken, msg.sender, firstPositionTokenId, FEE_TIER);
}
/// @dev Boundary ticks for the four ranges, walking away from the start
/// price in the direction that is "up" for the launch token.
function _trancheBounds(int24 poolTick, bool tokenIs0) internal pure returns (int24[5] memory b) {
if (tokenIs0) {
b[0] = _alignUp(poolTick, TICK_SPACING);
b[1] = b[0] + OFFSET_T1;
b[2] = b[0] + OFFSET_T2;
b[3] = b[0] + OFFSET_T3;
b[4] = (TickMath.MAX_TICK / TICK_SPACING) * TICK_SPACING;
if (b[3] >= b[4]) revert InvalidRange();
} else {
b[0] = _alignDown(poolTick, TICK_SPACING);
b[1] = b[0] - OFFSET_T1;
b[2] = b[0] - OFFSET_T2;
b[3] = b[0] - OFFSET_T3;
b[4] = (TickMath.MIN_TICK / TICK_SPACING) * TICK_SPACING;
if (b[3] <= b[4]) revert InvalidRange();
}
}
function _trancheAmounts() internal pure returns (uint256[4] memory a) {
a[0] = (TOTAL_SUPPLY * BPS_T1) / BPS;
a[1] = (TOTAL_SUPPLY * BPS_T2) / BPS;
a[2] = (TOTAL_SUPPLY * BPS_T3) / BPS;
a[3] = (TOTAL_SUPPLY * BPS_T4) / BPS;
}
function _mintTranches(StockFunToken t, PoolKey memory key, int24[5] memory b, bool tokenIs0)
internal
returns (uint256 firstPositionTokenId)
{
// PositionManager settles through Permit2.
t.approve(address(permit2), TOTAL_SUPPLY);
permit2.approve(address(t), address(positionManager), uint160(TOTAL_SUPPLY), type(uint48).max);
firstPositionTokenId = positionManager.nextTokenId();
uint256[4] memory amounts = _trancheAmounts();
bytes memory actions;
bytes[] memory params = new bytes[](TRANCHES + 1);
for (uint256 k = 0; k < TRANCHES; k++) {
(int24 lower, int24 upper) = b[k] < b[k + 1] ? (b[k], b[k + 1]) : (b[k + 1], b[k]);
uint128 liquidity = tokenIs0
? LiquidityAmounts.getLiquidityForAmount0(
TickMath.getSqrtPriceAtTick(lower), TickMath.getSqrtPriceAtTick(upper), amounts[k]
)
: LiquidityAmounts.getLiquidityForAmount1(
TickMath.getSqrtPriceAtTick(lower), TickMath.getSqrtPriceAtTick(upper), amounts[k]
);
actions = abi.encodePacked(actions, uint8(Actions.MINT_POSITION));
// A zero max on the stock side enforces one-sidedness: any stock owed reverts the mint.
params[k] = abi.encode(
key,
lower,
upper,
uint256(liquidity),
tokenIs0 ? uint128(amounts[k]) : 0,
tokenIs0 ? 0 : uint128(amounts[k]),
address(this),
bytes("")
);
}
actions = abi.encodePacked(actions, uint8(Actions.SETTLE_PAIR));
params[TRANCHES] = abi.encode(key.currency0, key.currency1);
positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp);
}
// ------------------------------------------------------------------ fees
/// @notice Collect accrued LP fees for a launch to `feeRecipient`. Callable by anyone.
function collectFees(uint256 launchId) external returns (uint256 amount0, uint256 amount1) {
Launch storage l = _launches[launchId];
address recipient = feeRecipient;
uint256 bal0Before = _balanceOf(l.poolKey.currency0, recipient);
uint256 bal1Before = _balanceOf(l.poolKey.currency1, recipient);
// A zero-liquidity decrease is a fee "poke" on each tranche position.
bytes memory actions;
bytes[] memory params = new bytes[](TRANCHES + 1);
for (uint256 k = 0; k < TRANCHES; k++) {
actions = abi.encodePacked(actions, uint8(Actions.DECREASE_LIQUIDITY));
params[k] = abi.encode(l.firstPositionTokenId + k, uint256(0), uint128(0), uint128(0), bytes(""));
}
actions = abi.encodePacked(actions, uint8(Actions.TAKE_PAIR));
params[TRANCHES] = abi.encode(l.poolKey.currency0, l.poolKey.currency1, recipient);
positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp);
amount0 = _balanceOf(l.poolKey.currency0, recipient) - bal0Before;
amount1 = _balanceOf(l.poolKey.currency1, recipient) - bal1Before;
emit FeesCollected(launchId, amount0, amount1);
}
// ---------------------------------------------------------------- rescue
/// @notice Testing-phase escape hatch: transfer a launch's position NFTs out.
/// Gone forever once disableRescue() is called.
function rescue(uint256 launchId, address to) external onlyOwner {
if (!rescueEnabled) revert RescueIsDisabled();
uint256 first = _launches[launchId].firstPositionTokenId;
for (uint256 k = 0; k < TRANCHES; k++) {
IERC721(address(positionManager)).transferFrom(address(this), to, first + k);
emit Rescued(launchId, first + k, to);
}
}
/// @notice Permanently disable rescue. Irreversible — after this every LP NFT is locked for good.
function disableRescue() external onlyOwner {
rescueEnabled = false;
emit RescueDisabled();
}
// ----------------------------------------------------------------- admin
function setLaunchFee(uint256 fee) external onlyOwner {
launchFee = fee;
emit LaunchFeeSet(fee);
}
function setFeeRecipient(address recipient) external onlyOwner {
feeRecipient = recipient;
emit FeeRecipientSet(recipient);
}
function withdrawETH(address to) external onlyOwner {
(bool ok,) = to.call{value: address(this).balance}("");
require(ok, "ETH_TRANSFER_FAILED");
}
// ----------------------------------------------------------------- views
function launchesCount() external view returns (uint256) {
return _launches.length;
}
function getLaunch(uint256 launchId) external view returns (Launch memory) {
return _launches[launchId];
}
// --------------------------------------------------------------- helpers
function _balanceOf(Currency currency, address account) internal view returns (uint256) {
return ERC20(Currency.unwrap(currency)).balanceOf(account);
}
/// @dev Solidity integer division truncates toward zero, so adjust explicitly per direction.
function _alignUp(int24 tick, int24 spacing) internal pure returns (int24 aligned) {
aligned = (tick / spacing) * spacing;
if (aligned < tick) aligned += spacing;
}
function _alignDown(int24 tick, int24 spacing) internal pure returns (int24 aligned) {
aligned = (tick / spacing) * spacing;
if (aligned > tick) aligned -= spacing;
}
}
Chain explorer4895msChain node90ms