EquifoldV3LaunchModule
0xfda86c98297811ea09273f6b80d8547a290872c6
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
9,691 bytes
ABI entries
37
19 read · 3 write
License
none
Contract information
- Address
- 0xfda86c98297811ea09273f6b80d8547a290872c6
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x7A27D4C9c3…aa9611dbFf
- Creation tx
- 0x0194bc7aa6…6b32ca2205
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (19)
creator() → address
feeCollector() → address
initialTick() → int24
isGraduated() → bool
isToken0() → bool
moduleFactory() → address
pairedPrincipal() → uint256
pool() → address
poolFee() → uint24
positionId() → uint256
positionManager() → address
progressToGraduation() → uint16
projectNumber() → uint256
standardTokenFactory() → address
swapRouter() → address
token() → address
treasury() → address
v3Factory() → address
weth() → address
Events (2)
FeesCollectedTokenInitialized
ABI
[
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "moduleFactory",
"type": "address"
},
{
"internalType": "address",
"name": "standardTokenFactory",
"type": "address"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address payable",
"name": "treasury",
"type": "address"
},
{
"internalType": "address",
"name": "feeCollector",
"type": "address"
},
{
"internalType": "contract IWrappedNative",
"name": "weth",
"type": "address"
},
{
"internalType": "address",
"name": "v3Factory",
"type": "address"
},
{
"internalType": "address",
"name": "positionManager",
"type": "address"
},
{
"internalType": "address",
"name": "swapRouter",
"type": "address"
},
{
"internalType": "uint256",
"name": "projectNumber",
"type": "uint256"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "initialTick",
"type": "int24"
}
],
"internalType": "struct EquifoldV3LaunchModule.Initialization",
"name": "init",
"type": "tuple"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyInitialized",
"type": "error"
},
{
"inputs": [],
"name": "DeadlineExpired",
"type": "error"
},
{
"inputs": [],
"name": "InvalidAddress",
"type": "error"
},
{
"inputs": [],
"name": "InvalidToken",
"type": "error"
},
{
"inputs": [],
"name": "NoFeesToCollect",
"type": "error"
},
{
"inputs": [],
"name": "NotInitialized",
"type": "error"
},
{
"inputs": [],
"name": "NotStandardTokenFactory",
"type": "error"
},
{
"inputs": [],
"name": "PoolNotAtLaunchPrice",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "UnexpectedEther",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "projectNumber",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "quoteAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"name": "FeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "projectNumber",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
}
],
"name": "TokenInitialized",
"type": "event"
},
{
"inputs": [],
"name": "collectFees",
"outputs": [
{
"internalType": "uint256",
"name": "quoteOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "creator",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "feeCollector",
"outputs": [
{
"internalType": "contract ILaunchpadTradingFeeCollector",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "initialBuy",
"outputs": [
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "initialTick",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token_",
"type": "address"
}
],
"name": "initializeToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "isGraduated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "isToken0",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "moduleFactory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pairedPrincipal",
"outputs": [
{
"internalType": "uint256",
"name": "principal",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolFee",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "progressToGraduation",
"outputs": [
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "projectNumber",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "standardTokenFactory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "swapRouter",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "token",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address payable",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "v3Factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "contract IWrappedNative",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
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 { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol";
import { IStandardTokenLaunchModule } from "../interfaces/IStandardTokenLaunchModule.sol";
import { ILaunchpadTradingFeeCollector } from "../interfaces/ILaunchpadTradingFeeCollector.sol";
import { IWrappedNative } from "../interfaces/IWrappedNative.sol";
import {
IUniswapV3PoolMinimal,
INonfungiblePositionManagerMinimal,
ISwapRouter02Minimal
} from "../interfaces/IUniswapV3Minimal.sol";
import { EquifoldRestrictedToken } from "../core/EquifoldRestrictedToken.sol";
import { LiquidityAmounts } from "./LiquidityAmounts.sol";
/// @dev Narrow callback so the module can tell its factory the real pool address once the pool is
/// created. The token/pool do not exist when the module itself is created, so the factory
/// cannot emit its DEX-discovery event at creation time; it emits it from here instead.
interface IEquifoldV3ModuleFactoryCallback {
function notifyPoolInitialized(
address token,
address pool
) external;
}
/// @title Equifold gen3 V3-native launch module
/// @author Equifold
/// @notice A single fixed-supply token's launch home on the SHARED Robinhood-chain Uniswap V3
/// fork. The module receives the full token allocation, seeds a single-sided concentrated
/// position (100% token, no quote) spanning from the launch tick to the top of the usable
/// range, and holds the position NFT FOREVER: there is intentionally NO function that
/// transfers, decreases, or burns it. Buyers push price up through the range like a bonding
/// curve; the position accrues the paired quote asset (WETH) which anyone can permissionlessly
/// crank via {collectFees}, split 70/30 creator/protocol using the launchpad's existing
/// fee-share machinery. "Graduation" here is a read-only milestone ({isGraduated}) — the
/// liquidity is already live and permanently locked from block one.
/// @dev The permanent lock is the ABSENCE of any exit path — audited as a product guarantee.
contract EquifoldV3LaunchModule is IStandardTokenLaunchModule, ReentrancyGuard {
using SafeERC20 for IERC20;
error AlreadyInitialized();
error DeadlineExpired();
error InvalidAddress();
error InvalidToken();
error NoFeesToCollect();
error NotInitialized();
error NotStandardTokenFactory();
error PoolNotAtLaunchPrice();
error UnexpectedEther();
uint16 internal constant BPS_DENOMINATOR = 10_000;
/// @notice Creator's share of every collected fee (quote and token side alike).
uint16 internal constant CREATOR_BPS = 7000;
/// @notice Protocol-remainder sub-splits of the quote side (of the 30% that is not the creator's).
uint16 internal constant PROTOCOL_TREASURY_BPS = 4000;
uint16 internal constant PROTOCOL_BUYBACK_BPS = 4000;
uint16 internal constant PROTOCOL_OPERATIONS_BPS = 1000;
/// @notice Top of the usable tick range for a 10000-fee (spacing-200) pool: (887272 / 200) * 200.
int24 internal constant MAX_USABLE_TICK = 887_200;
/// @notice Paired-quote principal at which the launch is considered graduated (read-only).
uint256 internal constant GRADUATION_THRESHOLD = 3.75 ether;
/// @dev Immutable configuration passed by {EquifoldV3LaunchModuleFactory} at creation.
struct Initialization {
address moduleFactory;
address standardTokenFactory;
address creator;
address payable treasury;
address feeCollector;
IWrappedNative weth;
address v3Factory;
address positionManager;
address swapRouter;
uint256 projectNumber;
uint24 poolFee;
int24 initialTick;
}
address public immutable moduleFactory;
address public immutable standardTokenFactory;
address public immutable creator;
address payable public immutable treasury;
ILaunchpadTradingFeeCollector public immutable feeCollector;
IWrappedNative public immutable weth;
address public immutable v3Factory;
address public immutable positionManager;
address public immutable swapRouter;
uint256 public immutable projectNumber;
uint24 public immutable poolFee;
/// @notice The token-perspective launch tick (`price = token per WETH`); sign-flipped at use so
/// the effective pool price is identical regardless of the token/WETH address sort.
int24 public immutable initialTick;
address public token;
address public pool;
uint256 public positionId;
/// @notice Whether the launch token sorts below WETH (is currency0 of the pool).
bool public isToken0;
event TokenInitialized(
uint256 indexed projectNumber,
address indexed token,
address indexed pool,
uint256 positionId
);
event FeesCollected(uint256 indexed projectNumber, uint256 quoteAmount, uint256 tokenAmount);
constructor(
Initialization memory init
) {
if (
init.moduleFactory == address(0) || init.standardTokenFactory == address(0)
|| init.creator == address(0) || init.treasury == address(0)
|| init.feeCollector == address(0) || address(init.weth) == address(0)
|| init.v3Factory == address(0) || init.positionManager == address(0)
|| init.swapRouter == address(0)
) revert InvalidAddress();
if (
init.moduleFactory.code.length == 0 || init.standardTokenFactory.code.length == 0
|| init.feeCollector.code.length == 0 || address(init.weth).code.length == 0
|| init.v3Factory.code.length == 0 || init.positionManager.code.length == 0
|| init.swapRouter.code.length == 0
) revert InvalidAddress();
if (init.projectNumber == 0 || init.poolFee == 0) revert InvalidAddress();
moduleFactory = init.moduleFactory;
standardTokenFactory = init.standardTokenFactory;
creator = init.creator;
treasury = init.treasury;
feeCollector = ILaunchpadTradingFeeCollector(init.feeCollector);
weth = init.weth;
v3Factory = init.v3Factory;
positionManager = init.positionManager;
swapRouter = init.swapRouter;
projectNumber = init.projectNumber;
poolFee = init.poolFee;
initialTick = init.initialTick;
}
/// @notice Binds the launch token, creates + initializes its pool at the launch price, and mints
/// the permanently-locked single-sided position holding the full supply.
/// @dev Callable once, only by the standard-token factory (the token's `launchContract`), which
/// has already transferred the full supply to this module in the same transaction.
function initializeToken(
address token_
) external nonReentrant {
if (msg.sender != standardTokenFactory) revert NotStandardTokenFactory();
if (token != address(0)) revert AlreadyInitialized();
if (token_ == address(0) || token_.code.length == 0) revert InvalidToken();
uint256 supply = IERC20(token_).totalSupply();
if (supply == 0 || IERC20(token_).balanceOf(address(this)) != supply) {
revert InvalidToken();
}
bool token0 = token_ < address(weth);
token = token_;
isToken0 = token0;
(address pool_, uint256 tokenId) = _openLockedPosition(token_, supply, token0);
pool = pool_;
positionId = tokenId;
emit TokenInitialized(projectNumber, token_, pool_, tokenId);
IEquifoldV3ModuleFactoryCallback(moduleFactory).notifyPoolInitialized(token_, pool_);
}
/// @dev Creates + initializes the pool at the launch price, verifies no hostile pre-creation,
/// then mints the single-sided position holding the full supply. Split out of
/// {initializeToken} to keep each frame's stack shallow.
function _openLockedPosition(
address token_,
uint256 supply,
bool token0
) private returns (address pool_, uint256 tokenId) {
// Token-perspective tick flips with the sort so the pool always opens at the same price.
int24 poolTick = token0 ? initialTick : -initialTick;
(address sorted0, address sorted1) =
token0 ? (token_, address(weth)) : (address(weth), token_);
uint160 targetSqrtPrice = TickMath.getSqrtPriceAtTick(poolTick);
pool_ = INonfungiblePositionManagerMinimal(positionManager)
.createAndInitializePoolIfNecessary(sorted0, sorted1, poolFee, targetSqrtPrice);
// Reject a hostile pre-created pool at a different price (stronger than a getPool pre-check).
(uint160 sqrtPriceX96,,,,,,) = IUniswapV3PoolMinimal(pool_).slot0();
if (sqrtPriceX96 != targetSqrtPrice) revert PoolNotAtLaunchPrice();
IERC20(token_).forceApprove(positionManager, supply);
tokenId = _mintSingleSided(sorted0, sorted1, token0, supply);
IERC20(token_).forceApprove(positionManager, 0);
}
/// @dev Mints the full-supply single-sided position spanning from the launch tick to the far end
/// of the usable range (all token, zero quote), owned by this module forever.
function _mintSingleSided(
address sorted0,
address sorted1,
bool token0,
uint256 supply
) private returns (uint256 tokenId) {
(int24 tickLower, int24 tickUpper) =
token0 ? (initialTick, MAX_USABLE_TICK) : (-MAX_USABLE_TICK, -initialTick);
(uint256 amount0Desired, uint256 amount1Desired) =
token0 ? (supply, uint256(0)) : (uint256(0), supply);
(tokenId,,,) = INonfungiblePositionManagerMinimal(positionManager)
.mint(
INonfungiblePositionManagerMinimal.MintParams({
token0: sorted0,
token1: sorted1,
fee: poolFee,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: amount0Desired,
amount1Desired: amount1Desired,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp
})
);
}
/// @notice Executes the creator's optional first market buy atomically with launch.
/// @dev Only the standard-token factory may call, in the same block as token deploy. Opens the
/// token's single-recipient launch-block exemption, routes the swap, then closes it — the
/// open/route/close is paired atomically so the exemption can never outlive this call.
function initialBuy(
uint256 minTokensOut,
address recipient,
uint256 deadline
) external payable nonReentrant returns (uint256 tokensOut) {
if (msg.sender != standardTokenFactory) revert NotStandardTokenFactory();
if (block.timestamp > deadline) revert DeadlineExpired();
address token_ = token;
if (token_ == address(0)) revert NotInitialized();
EquifoldRestrictedToken(token_).setInitialBuyRecipient(recipient);
tokensOut = ISwapRouter02Minimal(swapRouter).exactInputSingle{ value: msg.value }(
ISwapRouter02Minimal.ExactInputSingleParams({
tokenIn: address(weth),
tokenOut: token_,
fee: poolFee,
recipient: recipient,
amountIn: msg.value,
amountOutMinimum: minTokensOut,
sqrtPriceLimitX96: 0
})
);
EquifoldRestrictedToken(token_).setInitialBuyRecipient(address(0));
}
/// @notice Permissionlessly cranks the locked position's accrued fees to their fixed
/// destinations. Quote (WETH) is unwrapped and split 70% creator / 30% protocol through
/// the launchpad fee collector (identical claim machinery as curve tokens); the token
/// side goes 70% to the creator wallet directly and the remainder to the treasury.
/// @dev Intentionally has no access control and fixed destinations. Reverts if nothing accrued.
function collectFees() external nonReentrant returns (uint256 quoteOut, uint256 tokenOut) {
if (positionId == 0) revert NotInitialized();
(uint256 amount0, uint256 amount1) = INonfungiblePositionManagerMinimal(positionManager)
.collect(
INonfungiblePositionManagerMinimal.CollectParams({
tokenId: positionId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
(quoteOut, tokenOut) = isToken0 ? (amount1, amount0) : (amount0, amount1);
if (quoteOut == 0 && tokenOut == 0) revert NoFeesToCollect();
if (quoteOut > 0) _routeQuoteFees(quoteOut);
if (tokenOut > 0) _routeTokenFees(tokenOut);
emit FeesCollected(projectNumber, quoteOut, tokenOut);
}
/// @notice The WETH (quote) principal the permanently-locked position currently holds.
/// @dev Zero until initialized. Grows from zero as buyers push price up through the range.
function pairedPrincipal() public view returns (uint256 principal) {
if (positionId == 0) return 0;
(uint160 sqrtPriceX96,,,,,,) = IUniswapV3PoolMinimal(pool).slot0();
(,,,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) =
INonfungiblePositionManagerMinimal(positionManager).positions(positionId);
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
liquidity
);
principal = isToken0 ? amount1 : amount0;
}
/// @notice Progress toward the (read-only) graduation milestone, in basis points, capped at 100%.
function progressToGraduation() external view returns (uint16 bps) {
uint256 principal = pairedPrincipal();
if (principal >= GRADUATION_THRESHOLD) return BPS_DENOMINATOR;
return uint16(Math.mulDiv(principal, BPS_DENOMINATOR, GRADUATION_THRESHOLD));
}
/// @notice Whether the paired principal has reached the graduation threshold.
function isGraduated() external view returns (bool) {
return pairedPrincipal() >= GRADUATION_THRESHOLD;
}
/// @dev Unwraps the collected WETH and forwards the whole amount to the fee collector, split
/// 70/30. The protocol remainder's safety leg absorbs rounding dust so the FeeAmounts sum
/// EXACTLY equals `quoteAmount` — {LaunchpadFeeCollector.collectTradingFees} reverts otherwise.
function _routeQuoteFees(
uint256 quoteAmount
) private {
weth.withdraw(quoteAmount);
uint256 creatorAmount = (quoteAmount * CREATOR_BPS) / BPS_DENOMINATOR;
uint256 protocol = quoteAmount - creatorAmount;
uint256 treasuryAmount = (protocol * PROTOCOL_TREASURY_BPS) / BPS_DENOMINATOR;
uint256 buybackAmount = (protocol * PROTOCOL_BUYBACK_BPS) / BPS_DENOMINATOR;
uint256 operationsAmount = (protocol * PROTOCOL_OPERATIONS_BPS) / BPS_DENOMINATOR;
uint256 safetyAmount = protocol - treasuryAmount - buybackAmount - operationsAmount;
feeCollector.collectTradingFees{ value: quoteAmount }(
projectNumber,
true,
ILaunchpadTradingFeeCollector.FeeAmounts({
creator: creatorAmount,
treasury: treasuryAmount,
buyback: buybackAmount,
operations: operationsAmount,
safety: safetyAmount
})
);
}
/// @dev Splits the collected token side: 70% straight to the creator wallet, remainder to the
/// treasury (the remainder absorbs the rounding dust).
function _routeTokenFees(
uint256 tokenAmount
) private {
uint256 creatorAmount = (tokenAmount * CREATOR_BPS) / BPS_DENOMINATOR;
uint256 treasuryAmount = tokenAmount - creatorAmount;
IERC20(token).safeTransfer(creator, creatorAmount);
if (treasuryAmount > 0) IERC20(token).safeTransfer(treasury, treasuryAmount);
}
/// @notice Accepts ETH only from the WETH contract (the {collectFees} unwrap).
receive() external payable {
if (msg.sender != address(weth)) revert UnexpectedEther();
}
}
Chain explorer3225msChain node83ms