HoodsFactory
0x6175d09895d80b4eb73a7690445384fbe366a413
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
10,248 bytes
ABI entries
39
23 read · 7 write
License
none
Contract information
- Address
- 0x6175d09895d80b4eb73a7690445384fbe366a413
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x221e0B4809…340b0C1100
- Creation tx
- 0xf376f0a7ba…a743cee978
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (23)
CURVE_SUPPLY() → uint256
DEAD() → address
MAX_LAUNCH_FEE() → uint256
MAX_LAUNCH_PERIOD() → uint256
POOL_FEE() → uint24
TAIL_SUPPLY() → uint256
TICK_SPACING() → int24
TOTAL_SUPPLY() → uint256
VANITY_SUFFIX() → uint16
accruedFees() → uint256
creatorNFT() → address
hook() → address
isHoodsToken(address) → bool
launchFee() → uint256
launchPeriod() → uint256
owner() → address
permit2() → address
poolManager() → address
positionManager() → address
predictToken(address, bytes32) → address
tokenImplementation() → address
totalLaunches() → uint256
treasury() → address
Events (7)
LaunchFeeChangedLaunchFeePaidLaunchFeesCollectedLaunchPeriodChangedLaunchedOwnerChangedTreasuryChanged
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "address",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "address",
"name": "permit2_",
"type": "address"
},
{
"internalType": "address",
"name": "hook_",
"type": "address"
},
{
"internalType": "address",
"name": "creatorNFT_",
"type": "address"
},
{
"internalType": "address",
"name": "tokenImplementation_",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"name": "LaunchFeeChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "LaunchFeePaid",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "treasury",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "LaunchFeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "seconds_",
"type": "uint256"
}
],
"name": "LaunchPeriodChanged",
"type": "event"
},
{
"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": "int24",
"name": "graduationTick",
"type": "int24"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "metadataUri",
"type": "string"
}
],
"name": "Launched",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnerChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "treasury",
"type": "address"
}
],
"name": "TreasuryChanged",
"type": "event"
},
{
"inputs": [],
"name": "CURVE_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DEAD",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_LAUNCH_FEE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_LAUNCH_PERIOD",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TAIL_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": "VANITY_SUFFIX",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "accruedFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "collectLaunchFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "creatorNFT",
"outputs": [
{
"internalType": "contract HoodsCreatorNFT",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "contract IHooks",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isHoodsToken",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataUri",
"type": "string"
},
{
"internalType": "bytes32",
"name": "userSalt",
"type": "bytes32"
}
],
"name": "launch",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "launchFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "launchPeriod",
"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 IPermit2Minimal",
"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 IPositionManagerMinimal",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "bytes32",
"name": "userSalt",
"type": "bytes32"
}
],
"name": "predictToken",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"name": "setLaunchFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "seconds_",
"type": "uint256"
}
],
"name": "setLaunchPeriod",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "setOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "treasury_",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "tokenImplementation",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalLaunches",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
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 { Currency } from "v4-core/src/types/Currency.sol";
import { CurrencyLibrary } from "v4-core/src/types/Currency.sol";
import { IHooks } from "v4-core/src/interfaces/IHooks.sol";
import { BalanceDelta } from "v4-core/src/types/BalanceDelta.sol";
import { TickMath } from "v4-core/src/libraries/TickMath.sol";
import { LiquidityAmounts } from "v4-periphery/src/libraries/LiquidityAmounts.sol";
import { Actions } from "v4-periphery/src/libraries/Actions.sol";
import { HoodsToken } from "./HoodsToken.sol";
import { HoodsCreatorNFT } from "./HoodsCreatorNFT.sol";
import { CurveConfig } from "./CurveConfig.sol";
interface IHoodsHook {
function registerLaunch(PoolKey calldata key, int24 graduationTick) external;
}
interface IPositionManagerMinimal {
function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable;
}
interface IPermit2Minimal {
function approve(address token, address spender, uint160 amount, uint48 expiration) external;
}
interface IERC20Minimal {
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
/// @title HoodsFactory — atomic v4 launcher (arc §2.1, §3)
/// @notice One tx: CREATE2 vanity token (suffix `9999`, sender-bound salt) → pool init at the config's
/// price → single-sided full-supply position minted straight to 0xdEaD (provable permanent lock, no
/// locker) → Creator NFT to msg.sender → hook launch-registry → event. Configs are IMMUTABLE (arc §10:
/// Klik's mutable configs/hook/fee-receiver are all rejected). Native ETH is always currency0.
contract HoodsFactory is IUnlockCallback {
using CurrencyLibrary for Currency;
uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18; // 1B, minted across the two locked positions
uint256 public constant CURVE_SUPPLY = 800_000_000e18; // 80% → Position A (the curve, [grad, launch])
uint256 public constant TAIL_SUPPLY = 200_000_000e18; // 20% → Position B (the tail, [tail, grad])
uint24 public constant POOL_FEE = 0; // LP fee 0 — the hook takes the fee
int24 public constant TICK_SPACING = 200;
uint16 public constant VANITY_SUFFIX = 0x9999; // low 16 bits (hex-string suffix), branding only
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
IPoolManager public immutable poolManager;
IPositionManagerMinimal public immutable positionManager;
IPermit2Minimal public immutable permit2;
IHooks public immutable hook;
HoodsCreatorNFT public immutable creatorNFT;
address public immutable tokenImplementation;
uint256 public totalLaunches;
mapping(address => bool) public isHoodsToken; // registry (indexer / UI verification, arc §3.1)
// ── bounded owner: tunes the launch-limit window for FUTURE launches (baked per-token at launch,
// so existing tokens never change) and the flat deploy fee (hard-capped). Cannot mint, move LP,
// or seize tokens — no such code. ──
address public owner;
uint256 public launchPeriod = 30 minutes; // limit-window baked into each new token
uint256 public constant MAX_LAUNCH_PERIOD = 24 hours; // hard bound — limits can never trap a token
uint256 public launchFee = 0.0005 ether; // flat deploy fee per launch; 0 = free launches
uint256 public constant MAX_LAUNCH_FEE = 0.01 ether; // hard cap — the fee can never be extractive
uint256 public accruedFees; // collected launch fees awaiting sweep (pull model — no hot-path sends)
address public treasury; // launch-fee destination (owner-settable, never zero)
event OwnerChanged(address indexed previousOwner, address indexed newOwner);
event LaunchPeriodChanged(uint256 seconds_);
event LaunchFeeChanged(uint256 fee);
event TreasuryChanged(address indexed treasury);
event LaunchFeePaid(address indexed token, address indexed creator, uint256 amount);
event LaunchFeesCollected(address indexed treasury, uint256 amount);
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
/// Set the launch-limit window (seconds) for tokens launched AFTER this call. Bounded; never
/// affects already-live tokens (each bakes its window at launch).
function setLaunchPeriod(uint256 seconds_) external onlyOwner {
require(seconds_ <= MAX_LAUNCH_PERIOD, "too long");
launchPeriod = seconds_;
emit LaunchPeriodChanged(seconds_);
}
/// Set the flat deploy fee for FUTURE launches. Hard-capped at MAX_LAUNCH_FEE; 0 removes the fee.
function setLaunchFee(uint256 fee) external onlyOwner {
require(fee <= MAX_LAUNCH_FEE, "fee too high");
launchFee = fee;
emit LaunchFeeChanged(fee);
}
/// Rotate the launch-fee destination (e.g. to a multisig).
function setTreasury(address treasury_) external onlyOwner {
require(treasury_ != address(0), "zero treasury");
treasury = treasury_;
emit TreasuryChanged(treasury_);
}
/// Sweep accrued launch fees to the treasury. Permissionless — the destination is fixed, and a
/// failing treasury can never brick launches because fees accrue instead of being sent inline.
function collectLaunchFees() external {
uint256 amount = accruedFees;
require(amount > 0, "nothing accrued");
accruedFees = 0;
(bool ok,) = payable(treasury).call{ value: amount }("");
require(ok, "treasury send failed");
emit LaunchFeesCollected(treasury, amount);
}
/// Rotate owner (e.g. to a multisig) or renounce with address(0).
function setOwner(address newOwner) external onlyOwner {
emit OwnerChanged(owner, newOwner);
owner = newOwner;
}
event Launched(
address indexed token,
bytes32 indexed poolId,
address indexed creator,
int24 graduationTick,
string name,
string symbol,
string metadataUri
);
constructor(
address poolManager_,
address positionManager_,
address permit2_,
address hook_,
address creatorNFT_,
address tokenImplementation_,
address treasury_
) {
require(treasury_ != address(0), "zero treasury");
poolManager = IPoolManager(poolManager_);
positionManager = IPositionManagerMinimal(positionManager_);
permit2 = IPermit2Minimal(permit2_);
hook = IHooks(hook_);
creatorNFT = HoodsCreatorNFT(creatorNFT_);
tokenImplementation = tokenImplementation_;
treasury = treasury_;
owner = msg.sender;
emit OwnerChanged(address(0), msg.sender);
emit TreasuryChanged(treasury_);
}
/// `userSalt` is ground off-chain so predictToken(msg.sender, userSalt) ends in 0x9999. Salt is
/// sender-bound (stolen salts yield a different address). One fixed curve — no configId.
function launch(string calldata name, string calldata symbol, string calldata metadataUri, bytes32 userSalt)
external
payable
returns (address token, bytes32 poolId)
{
uint256 fee = launchFee; // snapshot — owner tuning mid-tx can't change this launch's terms
require(msg.value >= fee, "launch fee");
uint256 strayBefore = address(this).balance - msg.value; // any ETH resident BEFORE this call (not ours to refund)
uint160 sqrtLaunch = TickMath.getSqrtPriceAtTick(CurveConfig.TICK_LAUNCH);
// 1) deterministic vanity token
bytes32 salt = keccak256(abi.encodePacked(msg.sender, userSalt));
token = _clone(salt);
require(uint16(uint160(token)) == VANITY_SUFFIX, "vanity");
// full supply to the factory (it seeds the LP + dev-buy); creator + launch window baked in
HoodsToken(token).initialize(name, symbol, metadataUri, TOTAL_SUPPLY, address(this), msg.sender, launchPeriod);
// 2) pool: ETH (currency0) / token (currency1), LP fee 0, our hook
PoolKey memory key = PoolKey({
currency0: Currency.wrap(address(0)),
currency1: Currency.wrap(token),
fee: POOL_FEE,
tickSpacing: TICK_SPACING,
hooks: hook
});
poolManager.initialize(key, sqrtLaunch); // fires hook.beforeInitialize (records launch block)
poolId = keccak256(abi.encode(key));
IHoodsHook(address(hook)).registerLaunch(key, CurveConfig.GRADUATION_TICK);
// 3) two single-sided locked positions (80% curve + 20% tail) → 0xdEaD (permanent lock)
_mintLockedPositions(key, sqrtLaunch, token);
// 4) creator ownership NFT (fee-stream indirection)
creatorNFT.mint(msg.sender, uint256(uint160(token)));
// 5) take the deploy fee (accrues for a pull-model treasury sweep), then the optional atomic
// dev-buy with the REMAINDER — fee-free (launch block) and EXEMPT from the buy limits (the
// hook waives them for sender == factory, arc §5); tokens delivered to the creator.
if (fee > 0) {
accruedFees += fee;
emit LaunchFeePaid(token, msg.sender, fee);
}
uint256 devEth = msg.value - fee;
if (devEth > 0) _devBuy(key, token, devEth, msg.sender);
// burn any residual token dust the position didn't consume (tick-snap rounding)
uint256 dust = IERC20Minimal(token).balanceOf(address(this));
if (dust > 0) IERC20Minimal(token).transfer(DEAD, dust);
isHoodsToken[token] = true;
unchecked {
++totalLaunches;
}
emit Launched(token, poolId, msg.sender, CurveConfig.GRADUATION_TICK, name, symbol, metadataUri);
// refund only the UNCONSUMED portion of the dev-buy (e.g. a buy that walked the whole curve and
// hit the price limit) — NOT the whole balance; stray ETH sent to the factory isn't the
// launcher's to sweep (M-2), and this launch's fee stays accrued for the treasury. Last, after
// all state writes (CEI).
uint256 refund = address(this).balance - strayBefore - fee;
if (refund > 0) {
(bool ok,) = payable(msg.sender).call{ value: refund }("");
require(ok, "refund failed");
}
}
// ── dev-buy via the PoolManager unlock pattern (self-contained; buyer = creator via hookData) ──
function _devBuy(PoolKey memory key, address token, uint256 ethIn, address buyer) private {
poolManager.unlock(abi.encode(key, token, ethIn, buyer));
}
/// Only the PoolManager, only mid-launch (transient flag), executes the ETH→token dev swap.
function unlockCallback(bytes calldata data) external returns (bytes memory) {
require(msg.sender == address(poolManager), "not manager");
(PoolKey memory key, address token, uint256 ethIn, address buyer) =
abi.decode(data, (PoolKey, address, uint256, address));
// zeroForOne (ETH→token) exact-in; recipient passed to the hook for recipient-keyed limits
BalanceDelta delta = poolManager.swap(
key,
IPoolManager.SwapParams({ zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: 4295128740 }),
abi.encode(buyer)
);
// settle the ETH we owe (native) and take the token out to the creator
uint256 ethOwed = uint256(int256(-delta.amount0()));
poolManager.settle{ value: ethOwed }();
int128 tokenOut = delta.amount1();
if (tokenOut > 0) poolManager.take(Currency.wrap(token), buyer, uint256(int256(tokenOut)));
return "";
}
receive() external payable { } // dev-buy ETH refunds / native settle change
/// Two single-sided token positions, both minted to 0xdEaD (permanent lock) in one batch:
/// Position A (curve, 80%) over [GRADUATION_TICK, TICK_LAUNCH]; Position B (tail, 20%) over
/// [TICK_TAIL, GRADUATION_TICK]. At the launch price (= sqrt at TICK_LAUNCH) both ranges sit at/
/// below the current tick, so each is 100% token (currency1) — buys walk the tick down, selling A
/// first (→ graduation at GRADUATION_TICK), then B.
function _mintLockedPositions(PoolKey memory key, uint160 sqrtLaunch, address token) private {
uint160 sqrtGrad = TickMath.getSqrtPriceAtTick(CurveConfig.GRADUATION_TICK);
uint160 sqrtTail = TickMath.getSqrtPriceAtTick(CurveConfig.TICK_TAIL);
uint128 liqA = LiquidityAmounts.getLiquidityForAmounts(
sqrtLaunch, sqrtGrad, sqrtLaunch, 0, CURVE_SUPPLY // Position A range [grad, launch]
);
uint128 liqB = LiquidityAmounts.getLiquidityForAmounts(
sqrtLaunch, sqrtTail, sqrtGrad, 0, TAIL_SUPPLY // Position B range [tail, grad]
);
// approve the token to be pulled by the PositionManager via Permit2
IERC20Minimal(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.SETTLE_PAIR));
bytes[] memory params = new bytes[](3);
params[0] = abi.encode(
key, CurveConfig.GRADUATION_TICK, CurveConfig.TICK_LAUNCH, liqA, uint256(0), CURVE_SUPPLY, DEAD, bytes("")
);
params[1] = abi.encode(
key, CurveConfig.TICK_TAIL, CurveConfig.GRADUATION_TICK, liqB, uint256(0), TAIL_SUPPLY, DEAD, bytes("")
);
params[2] = abi.encode(key.currency0, key.currency1); // settle both; currency0(ETH)=0 owed
positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp + 60);
}
// ── CREATE2 clones (EIP-1167) ──
function _clone(bytes32 salt) private returns (address instance) {
bytes memory code = _cloneCode();
assembly {
instance := create2(0, add(code, 0x20), mload(code), salt)
}
require(instance != address(0), "clone failed");
}
/// Predict the token address for (creator, userSalt) — the off-chain vanity miner target.
function predictToken(address creator, bytes32 userSalt) external view returns (address) {
bytes32 salt = keccak256(abi.encodePacked(creator, userSalt));
bytes32 h = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(_cloneCode())));
return address(uint160(uint256(h)));
}
function _cloneCode() private view returns (bytes memory) {
return abi.encodePacked(
hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", tokenImplementation, hex"5af43d82803e903d91602b57fd5bf3"
);
}
}
Chain explorer4512msChain node102ms