HoodCommunityFactory
0x6d188c21f99a7578d4e3fd449b67d740ff97eb4d
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
3,247 bytes
ABI entries
24
10 read · 4 write
License
none
Contract information
- Address
- 0x6d188c21f99a7578d4e3fd449b67d740ff97eb4d
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x0F307b093d…D00D9a7836
- Creation tx
- 0xbc30fffcf4…4638760250
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (10)
allCommunityTokens(uint256) → address
communityCount() → uint256
implementation() → address
isCommunity(address) → bool
launchpad() → address
owner() → address
predictRewards(address, bytes32) → address
publisher() → address
rewardsOf(address) → address
weth() → address
Events (3)
CommunityLaunchedOwnershipTransferredPublisherSet
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "launchpad_",
"type": "address"
},
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "address",
"name": "owner_",
"type": "address"
},
{
"internalType": "address",
"name": "publisher_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "FailedDeployment",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "InsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "rewards",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "launcher",
"type": "address"
}
],
"name": "CommunityLaunched",
"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": "address",
"name": "publisher",
"type": "address"
}
],
"name": "PublisherSet",
"type": "event"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allCommunityTokens",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "communityCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "implementation",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "isCommunity",
"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": "salt",
"type": "bytes32"
},
{
"internalType": "uint16",
"name": "tradeFeeBps",
"type": "uint16"
},
{
"internalType": "uint256",
"name": "totalSupply_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "devBuyEth",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minDevTokensOut",
"type": "uint256"
}
],
"name": "launchCommunity",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "rewards",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "launchpad",
"outputs": [
{
"internalType": "contract ICommunityLaunchpad",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "caller",
"type": "address"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"name": "predictRewards",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "publisher",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "rewardsOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "publisher_",
"type": "address"
}
],
"name": "setPublisher",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface ICommunityLaunchpad {
function createTokenFor(
address creator,
string calldata name,
string calldata symbol,
string calldata metadataURI,
uint256 minTokensOut,
bytes32 salt,
uint16 tradeFeeBps,
uint256 totalSupply_
) external payable returns (address token);
function buyFor(address token, address recipient, uint256 minTokensOut) external payable;
function claimCreatorFees(address token) external;
function creatorFees(address token) external view returns (uint256);
// config tuple: (virtualEthSeed, creationFee, defaultTradeFeeBps, migrationFee,
// migrationFeeBps, guardBlocks, guardMaxWalletBps, creatorFeeShareBps, vanityEnforced)
function config()
external
view
returns (uint128, uint64, uint16, uint64, uint16, uint16, uint16, uint16, bool);
}
interface IWETH9 {
function withdraw(uint256) external;
function balanceOf(address) external view returns (uint256);
}
interface ICommunityFactory {
function publisher() external view returns (address);
}
/// @title HoodCommunityRewards
/// @notice One instance is cloned per COMMUNITY coin and set as that coin's
/// `creator` on the launchpad. Every creator fee the coin earns — the
/// ETH curve fees and the WETH creator-share of the locked Uniswap pool —
/// lands here and is **AUTO-DISTRIBUTED** to the coin's HOLDERS pro-rata.
/// There is NO claiming: the hood.fun keeper harvests the fees, snapshots
/// holders off-chain, and calls `distribute()`, which pays every holder
/// directly and reimburses its own gas out of the pot — so it runs forever
/// with no funding and nobody ever has to claim.
///
/// The token-side burn is UNCHANGED — it still happens in the locker.
/// Only the ETH/WETH creator-fee side is redirected to holders here.
contract HoodCommunityRewards is ReentrancyGuard {
ICommunityLaunchpad public launchpad;
address public factory; // its publisher() is the authorized keeper
address public token;
IWETH9 public weth;
uint256 public totalReceived; // all ETH ever harvested
uint256 public totalDistributed; // all ETH ever paid out to holders
uint256 public round; // distribution counter
bool private _init;
/// @dev Gas cap per holder transfer — enough for an EOA/simple receiver, too
/// little for a griefing contract to burn or re-enter. A recipient that
/// needs more just gets skipped and rolls to the next round.
uint256 public constant SEND_GAS = 30_000;
/// @dev The keeper's gas reimbursement is capped at this share of a batch, so
/// a compromised keeper can never siphon the pot disguised as "gas".
uint256 public constant MAX_GAS_BPS = 1000; // 10%
event Harvested(uint256 amount, uint256 totalReceived);
event Distributed(uint256 indexed round, uint256 toHolders, uint256 recipients, uint256 gasReimbursed);
event Paid(address indexed holder, uint256 amount);
event Skipped(address indexed holder, uint256 amount); // recipient rejected the ETH
error AlreadyInitialized();
error NotPublisher();
error LengthMismatch();
error InsufficientBalance();
/// @dev Called once by the factory right after cloning.
function initialize(address launchpad_, address factory_, address token_, address weth_) external {
if (_init) revert AlreadyInitialized();
_init = true;
launchpad = ICommunityLaunchpad(launchpad_);
factory = factory_;
token = token_;
weth = IWETH9(weth_);
}
/// @notice Accept ETH (creator fees pulled from the launchpad, or unwrapped WETH).
receive() external payable {}
/// @notice Pull everything owed to this coin into the pot. Permissionless — the
/// keeper (or anyone) can top up. Harvests curve creator fees and
/// unwraps any WETH the locker has paid in.
function harvest() public nonReentrant returns (uint256 gained) {
return _harvest();
}
function _harvest() internal returns (uint256 gained) {
uint256 before = address(this).balance;
if (launchpad.creatorFees(token) > 0) {
launchpad.claimCreatorFees(token); // sends ETH here
}
uint256 w = weth.balanceOf(address(this));
if (w > 0) weth.withdraw(w); // WETH → ETH
gained = address(this).balance - before;
if (gained > 0) {
totalReceived += gained;
emit Harvested(gained, totalReceived);
}
}
/// @notice Keeper-only: auto-send `amounts[i]` to `holders[i]` — the pro-rata
/// shares the keeper computed off-chain from a live holder snapshot.
/// Harvests first, so it always pays out fresh fees. A recipient whose
/// transfer reverts (a contract that rejects ETH) is skipped and its
/// share simply stays in the pot for next round. The keeper's gas is
/// reimbursed from the pot (capped at MAX_GAS_BPS of the batch), so the
/// distribution funds its own gas and never needs topping up.
function distribute(address[] calldata holders, uint256[] calldata amounts) external nonReentrant {
if (msg.sender != ICommunityFactory(factory).publisher()) revert NotPublisher();
if (holders.length != amounts.length) revert LengthMismatch();
uint256 gasStart = gasleft();
_harvest(); // pull any pending fees into the pot first
uint256 total;
for (uint256 i; i < amounts.length; ++i) total += amounts[i];
if (total > address(this).balance) revert InsufficientBalance();
uint256 paid;
uint256 recipients;
for (uint256 i; i < holders.length; ++i) {
uint256 amt = amounts[i];
if (amt == 0) continue;
(bool ok,) = holders[i].call{value: amt, gas: SEND_GAS}("");
if (ok) {
paid += amt;
unchecked {
++recipients;
}
emit Paid(holders[i], amt);
} else {
emit Skipped(holders[i], amt); // rolls to next round
}
}
totalDistributed += paid;
unchecked {
++round;
}
// Reimburse the keeper's gas out of the pot, capped so it can never be an
// extraction path. +40k covers the reimburse call + trailing opcodes.
uint256 gasCost = (gasStart - gasleft() + 40_000) * tx.gasprice;
uint256 cap = (paid * MAX_GAS_BPS) / 10_000;
if (gasCost > cap) gasCost = cap;
if (gasCost > address(this).balance) gasCost = address(this).balance;
if (gasCost > 0) {
(bool r,) = msg.sender.call{value: gasCost}("");
r; // best-effort; on failure the ETH just stays in the pot
}
emit Distributed(round, paid, recipients, gasCost);
}
/// @notice ETH sitting in the pot right now (already-harvested, awaiting the
/// next distribution) plus everything still pending on the launchpad —
/// i.e. what the next distribution will pay out. Lets the keeper decide
/// when the pot is worth a distribution.
function distributable() external view returns (uint256) {
return address(this).balance + launchpad.creatorFees(token) + weth.balanceOf(address(this));
}
}
/// @title HoodCommunityFactory
/// @notice Whitelisted platform that launches COMMUNITY coins on the hood.fun
/// launchpad. For each launch it clones a HoodCommunityRewards, makes the
/// clone the coin's `creator` (so fees flow to holders), and — if the
/// creator wants a first buy — routes the dev-buy to the real launcher
/// (not the clone) via the guard-exempt platform buyFor.
///
/// The owner (Safe) sets the keeper `publisher` that signs reward roots.
contract HoodCommunityFactory is Ownable, ReentrancyGuard {
ICommunityLaunchpad public immutable launchpad;
address public immutable weth;
address public immutable implementation;
/// @notice Keeper allowed to publish reward roots on every clone.
address public publisher;
mapping(address token => address) public rewardsOf; // token → its rewards clone
address[] public allCommunityTokens;
event CommunityLaunched(address indexed token, address indexed rewards, address indexed launcher);
event PublisherSet(address publisher);
error ZeroAddress();
constructor(address launchpad_, address weth_, address owner_, address publisher_) Ownable(owner_) {
if (launchpad_ == address(0) || weth_ == address(0) || publisher_ == address(0)) revert ZeroAddress();
launchpad = ICommunityLaunchpad(launchpad_);
weth = weth_;
publisher = publisher_;
implementation = address(new HoodCommunityRewards());
}
function setPublisher(address publisher_) external onlyOwner {
if (publisher_ == address(0)) revert ZeroAddress();
publisher = publisher_;
emit PublisherSet(publisher_);
}
/// @notice Launch a community coin. `salt` must be ground so the token address
/// ends in 0x600d (grind against THIS factory's address, since it is
/// the launchpad caller). msg.value = creationFee + devBuyEth.
function launchCommunity(
string calldata name,
string calldata symbol,
string calldata metadataURI,
bytes32 salt,
uint16 tradeFeeBps,
uint256 totalSupply_,
uint256 devBuyEth,
uint256 minDevTokensOut
) external payable nonReentrant returns (address token, address rewards) {
// clone the per-coin rewards vault (deterministic, unique per caller+salt)
rewards = Clones.cloneDeterministic(implementation, keccak256(abi.encode(msg.sender, salt)));
// Pass EXACTLY the creation fee to the launchpad so it never dev-buys to
// the clone. (createTokenFor treats any value above the fee as a dev-buy
// credited to the `creator` — here the clone — which we must avoid.)
(, uint64 creationFee,,,,,,,) = launchpad.config();
uint256 fee = uint256(creationFee);
require(msg.value >= fee + devBuyEth, "insufficient value");
token = launchpad.createTokenFor{value: fee}(
rewards, name, symbol, metadataURI, 0, salt, tradeFeeBps, totalSupply_
);
HoodCommunityRewards(payable(rewards)).initialize(address(launchpad), address(this), token, weth);
// optional creator dev-buy — credited to the LAUNCHER, guard-exempt
if (devBuyEth > 0) {
launchpad.buyFor{value: devBuyEth}(token, msg.sender, minDevTokensOut);
}
// refund any dust the caller overpaid
uint256 refund = msg.value - fee - devBuyEth;
if (refund > 0) {
(bool ok,) = msg.sender.call{value: refund}("");
require(ok, "refund failed");
}
rewardsOf[token] = rewards;
allCommunityTokens.push(token);
emit CommunityLaunched(token, rewards, msg.sender);
}
function isCommunity(address token) external view returns (bool) {
return rewardsOf[token] != address(0);
}
function communityCount() external view returns (uint256) {
return allCommunityTokens.length;
}
/// @notice Predict a launch's rewards-clone address for a given caller + salt.
function predictRewards(address caller, bytes32 salt) external view returns (address) {
return Clones.predictDeterministicAddress(implementation, keccak256(abi.encode(caller, salt)), address(this));
}
}
Chain explorer3298msChain node79ms