LaunchHoodV3Locker
0x25cf5b90e2b141142f868fb4e52ec66466287a75
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
5,244 bytes
ABI entries
29
9 read · 5 write
License
none
Contract information
- Address
- 0x25cf5b90e2b141142f868fb4e52ec66466287a75
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x73267feDc2…B208cfEaA4
- Creation tx
- 0x6ee57622f0…471fad0523
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (9)
FACTORY() → address
POSITION_MANAGER() → address
PROTOCOL_TREASURY() → address
defaultCreatorShareBps() → uint16
onERC721Received(address, address, uint256, bytes) → bytes4
positionMeta(uint256) → address, address, address, uint16
positionOf(address) → uint256
rewardRecipient(uint256) → address
rewardRecipientOverride(uint256) → address
Events (4)
DefaultCreatorShareUpdatedFeesCollectedRegisteredRewardRecipientChanged
ABI
[
{
"inputs": [
{
"internalType": "contract INonfungiblePositionManagerMinimal",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "address",
"name": "factory_",
"type": "address"
},
{
"internalType": "uint16",
"name": "initialCreatorShareBps_",
"type": "uint16"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyRegistered",
"type": "error"
},
{
"inputs": [],
"name": "InvalidShare",
"type": "error"
},
{
"inputs": [],
"name": "OnlyAdmin",
"type": "error"
},
{
"inputs": [],
"name": "OnlyFactory",
"type": "error"
},
{
"inputs": [],
"name": "OnlyPositionManager",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "UnexpectedETH",
"type": "error"
},
{
"inputs": [],
"name": "UnknownPosition",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "creatorShareBps",
"type": "uint16"
}
],
"name": "DefaultCreatorShareUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "meme",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalMeme",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "creatorEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "creatorMeme",
"type": "uint256"
}
],
"name": "FeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "meme",
"type": "address"
},
{
"indexed": false,
"internalType": "uint16",
"name": "creatorShareBps",
"type": "uint16"
}
],
"name": "Registered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "meme",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "RewardRecipientChanged",
"type": "event"
},
{
"inputs": [],
"name": "FACTORY",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POSITION_MANAGER",
"outputs": [
{
"internalType": "contract INonfungiblePositionManagerMinimal",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PROTOCOL_TREASURY",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "collect",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256[]",
"name": "tokenIds",
"type": "uint256[]"
}
],
"name": "collectMany",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "defaultCreatorShareBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "positionMeta",
"outputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "meme",
"type": "address"
},
{
"internalType": "address",
"name": "pairToken",
"type": "address"
},
{
"internalType": "uint16",
"name": "creatorShareBps",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "meme",
"type": "address"
}
],
"name": "positionOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "meme",
"type": "address"
},
{
"internalType": "address",
"name": "pairToken",
"type": "address"
}
],
"name": "register",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "rewardRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "rewardRecipientOverride",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "newShareBps",
"type": "uint16"
}
],
"name": "setDefaultCreatorShareBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "setRewardRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// =============================================================================
// launchhood.fun — memecoin launchpad on Ethereum
// https://launchhood.fun · https://x.com/LaunchHoodfun
// =============================================================================
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {INonfungiblePositionManagerMinimal, IWETH9Minimal} from "./interfaces/IUniswapV3.sol";
/// @title LaunchHoodV3Locker
/// @notice Holds every V3-direct launch's position NFT forever. Principal
/// liquidity is structurally non-withdrawable — only fee collection
/// is exposed. Fees split by a single creator/protocol share that is
/// SNAPSHOTTED into each position at launch and frozen there: the
/// protocol admin (factory owner) can retune the default for future
/// launches, but existing positions keep the split they launched
/// with, and creators can never influence it.
///
/// V3 pools accrue fees in both pool currencies. collect() harvests
/// both; the WETH side is unwrapped and paid out as ETH, the token
/// side is transferred as-is.
///
/// The creator's share goes to `rewardRecipient` — the creator by
/// default, but the protocol admin (factory owner) can redirect it,
/// e.g. to a community-lead address.
contract LaunchHoodV3Locker is ReentrancyGuard, IERC721Receiver {
using SafeERC20 for IERC20;
INonfungiblePositionManagerMinimal public immutable POSITION_MANAGER;
address public immutable PROTOCOL_TREASURY;
address public immutable FACTORY;
struct PositionMeta {
address creator;
address meme;
address pairToken; // WETH — unwrapped to ETH on payout
uint16 creatorShareBps; // frozen at registration
}
/// @notice Creator share (bps) applied to NEW launches. Snapshotted into
/// PositionMeta at register() — changing it never touches
/// already-locked positions.
uint16 public defaultCreatorShareBps;
mapping(uint256 tokenId => PositionMeta) public positionMeta;
/// @notice Position id for a launched token (one lock per launch).
mapping(address meme => uint256) public positionOf;
/// @notice Admin-set override of who receives the creator share.
/// address(0) = no override (the original creator earns).
mapping(uint256 tokenId => address) public rewardRecipientOverride;
error OnlyPositionManager();
error OnlyFactory();
error OnlyAdmin();
error AlreadyRegistered();
error UnknownPosition();
error InvalidShare();
error UnexpectedETH();
event Registered(
uint256 indexed tokenId, address indexed creator, address indexed meme, uint16 creatorShareBps
);
event DefaultCreatorShareUpdated(uint16 creatorShareBps);
event RewardRecipientChanged(uint256 indexed tokenId, address indexed meme, address indexed newRecipient);
event FeesCollected(
uint256 indexed tokenId,
address indexed meme,
uint256 totalEth,
uint256 totalMeme,
uint256 creatorEth,
uint256 creatorMeme
);
constructor(
INonfungiblePositionManagerMinimal positionManager_,
address treasury_,
address factory_,
uint16 initialCreatorShareBps_
) {
if (initialCreatorShareBps_ > 10_000) revert InvalidShare();
POSITION_MANAGER = positionManager_;
PROTOCOL_TREASURY = treasury_;
FACTORY = factory_;
defaultCreatorShareBps = initialCreatorShareBps_;
}
/// @notice Protocol admin (= factory owner) sets the split for FUTURE
/// launches. Already-locked positions keep their frozen share.
function setDefaultCreatorShareBps(uint16 newShareBps) external {
if (msg.sender != Ownable(FACTORY).owner()) revert OnlyAdmin();
if (newShareBps > 10_000) revert InvalidShare();
defaultCreatorShareBps = newShareBps;
emit DefaultCreatorShareUpdated(newShareBps);
}
/// @notice Called by FACTORY exactly once per launch, right after the
/// position NFT is minted straight to this contract. The split is
/// snapshotted from `defaultCreatorShareBps` — neither the caller
/// nor the creator can pass a custom one.
function register(uint256 tokenId, address creator, address meme, address pairToken) external {
if (msg.sender != FACTORY) revert OnlyFactory();
if (positionMeta[tokenId].creator != address(0)) revert AlreadyRegistered();
uint16 share = defaultCreatorShareBps;
positionMeta[tokenId] = PositionMeta(creator, meme, pairToken, share);
positionOf[meme] = tokenId;
emit Registered(tokenId, creator, meme, share);
}
/// @notice Protocol admin (= factory owner) may redirect the creator
/// share of a position's fees, e.g. to a community lead. Set
/// address(0) to restore the original creator.
function setRewardRecipient(uint256 tokenId, address newRecipient) external {
if (msg.sender != Ownable(FACTORY).owner()) revert OnlyAdmin();
PositionMeta memory pm = positionMeta[tokenId];
if (pm.creator == address(0)) revert UnknownPosition();
rewardRecipientOverride[tokenId] = newRecipient;
emit RewardRecipientChanged(
tokenId, pm.meme, newRecipient == address(0) ? pm.creator : newRecipient
);
}
/// @notice Effective receiver of the creator share for a position.
function rewardRecipient(uint256 tokenId) public view returns (address) {
address r = rewardRecipientOverride[tokenId];
return r == address(0) ? positionMeta[tokenId].creator : r;
}
function collect(uint256 tokenId) external nonReentrant {
_collect(tokenId);
}
function collectMany(uint256[] calldata tokenIds) external nonReentrant {
uint256 len = tokenIds.length;
for (uint256 i; i < len; ++i) {
_collect(tokenIds[i]);
}
}
function _collect(uint256 tokenId) internal {
PositionMeta memory pm = positionMeta[tokenId];
if (pm.creator == address(0)) revert UnknownPosition();
(uint256 amount0, uint256 amount1) = POSITION_MANAGER.collect(
INonfungiblePositionManagerMinimal.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
(uint256 pairAmount, uint256 memeAmount) =
pm.pairToken < pm.meme ? (amount0, amount1) : (amount1, amount0);
// Pay the pair side out as native ETH.
if (pairAmount > 0) {
IWETH9Minimal(pm.pairToken).withdraw(pairAmount);
}
address recipient = rewardRecipient(tokenId);
// Odd wei rounds toward the protocol.
uint256 creatorEth = (pairAmount * pm.creatorShareBps) / 10_000;
uint256 creatorMeme = (memeAmount * pm.creatorShareBps) / 10_000;
uint256 protoEth = pairAmount - creatorEth;
uint256 protoMeme = memeAmount - creatorMeme;
// A hostile recipient (contract rejecting ETH) can't brick collect —
// their ETH share falls through to the protocol.
if (creatorEth > 0) {
(bool ok,) = recipient.call{value: creatorEth}("");
if (!ok) protoEth += creatorEth;
}
if (creatorMeme > 0) {
IERC20(pm.meme).safeTransfer(recipient, creatorMeme);
}
if (protoEth > 0) {
(bool ok,) = PROTOCOL_TREASURY.call{value: protoEth}("");
require(ok, "treasury ETH send failed");
}
if (protoMeme > 0) {
IERC20(pm.meme).safeTransfer(PROTOCOL_TREASURY, protoMeme);
}
emit FeesCollected(tokenId, pm.meme, pairAmount, memeAmount, creatorEth, creatorMeme);
}
function onERC721Received(address, address, uint256, bytes calldata)
external
view
override
returns (bytes4)
{
if (msg.sender != address(POSITION_MANAGER)) revert OnlyPositionManager();
return IERC721Receiver.onERC721Received.selector;
}
/// @dev Accept ETH only from WETH.withdraw during _collect.
receive() external payable {
// pm.pairToken varies per position, so allow any contract sender —
// but plain EOA transfers are rejected to avoid stray deposits.
if (msg.sender.code.length == 0) revert UnexpectedETH();
}
}
Chain explorer4211msChain node97ms