PoolFactory
0x8059985b13a2aadef884e8b01fe3ce147352b575
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
19,660 bytes
ABI entries
34
11 read · 13 write
License
none
Contract information
- Address
- 0x8059985b13a2aadef884e8b01fe3ce147352b575
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x384D927764…ae498e78b3
- Creation tx
- 0xb5f3e163d8…c86d620259
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (11)
ADMIN_ROLE() → bytes32
DEFAULT_ADMIN_ROLE() → bytes32
MAX_DURATION() → uint256
OPERATION_ROLE() → bytes32
config() → address, uint256, uint256, uint256, address, uint256, uint256, uint256, uint256
createPaused() → bool
getRoleAdmin(bytes32) → bytes32
hasRole(bytes32, address) → bool
poolCount() → uint256
pools(uint256) → address
supportsInterface(bytes4) → bool
Events (6)
ConfigUpdatedCreatePausedSetPoolCreatedRoleAdminChangedRoleGrantedRoleRevoked
ABI
[
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "virtualToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "reserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxContribution",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minContribution",
"type": "uint256"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "feeAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "duration",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenTotalSupply",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenLpSupply",
"type": "uint256"
}
],
"internalType": "struct PoolFactory.Config",
"name": "c",
"type": "tuple"
},
{
"internalType": "address",
"name": "admin",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AccessControlBadConfirmation",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "bytes32",
"name": "neededRole",
"type": "bytes32"
}
],
"name": "AccessControlUnauthorizedAccount",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"anonymous": false,
"inputs": [],
"name": "ConfigUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "CreatePausedSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "devRecipient",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "ticker",
"type": "string"
}
],
"name": "PoolCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "previousAdminRole",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "newAdminRole",
"type": "bytes32"
}
],
"name": "RoleAdminChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "RoleGranted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "RoleRevoked",
"type": "event"
},
{
"inputs": [],
"name": "ADMIN_ROLE",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DEFAULT_ADMIN_ROLE",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_DURATION",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "OPERATION_ROLE",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
}
],
"name": "cancelPool",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "address",
"name": "unlocker",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "actualIds",
"type": "uint256[]"
},
{
"internalType": "uint256",
"name": "batchId",
"type": "uint256"
}
],
"name": "claimUnlocked",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "config",
"outputs": [
{
"internalType": "address",
"name": "virtualToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "reserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxContribution",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minContribution",
"type": "uint256"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "feeAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "duration",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenTotalSupply",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenLpSupply",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "createPaused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "ticker",
"type": "string"
},
{
"internalType": "address",
"name": "devRecipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "startTime",
"type": "uint256"
}
],
"internalType": "struct PoolCreationParams",
"name": "p",
"type": "tuple"
}
],
"name": "createPool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
}
],
"name": "getRoleAdmin",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "grantRole",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "hasRole",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "pools",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "releaseForLaunch",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "callerConfirmation",
"type": "address"
}
],
"name": "renounceRole",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "newStart",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "newEnd",
"type": "uint256"
}
],
"name": "resetWindow",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "revokeRole",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "virtualToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "reserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxContribution",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minContribution",
"type": "uint256"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "feeAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "duration",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenTotalSupply",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenLpSupply",
"type": "uint256"
}
],
"internalType": "struct PoolFactory.Config",
"name": "c",
"type": "tuple"
}
],
"name": "setConfig",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "setCreatePaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "uint256[]",
"name": "participantIndexes",
"type": "uint256[]"
}
],
"name": "settleFailed",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "address",
"name": "launchedToken",
"type": "address"
},
{
"components": [
{
"internalType": "address[]",
"name": "refundAddresses",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "refundAmounts",
"type": "uint256[]"
},
{
"internalType": "address[]",
"name": "distributeAddresses",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "distributeAmounts",
"type": "uint256[]"
}
],
"internalType": "struct PoolSettlement",
"name": "s",
"type": "tuple"
}
],
"name": "settleSuccess",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "address",
"name": "tkn",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "sweep",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Pool} from "./Pool.sol";
import {PoolCreationParams, PoolInitParams, PoolSettlement} from "./PoolTypes.sol";
/// @title PoolFactory — opens Community Pools and is the operator surface.
/// @notice Forked from Virtuals' FGenesis (MIT). `createPool` is permissionless
/// (anyone can open a pool for any dev, paying the configured $VIRT
/// create fee — 0 = free). The
/// operator finalizes each pool after its window via settleSuccess /
/// settleFailed (amounts computed off-chain). One global Config holds
/// the frozen economics (50K target, 500/wallet cap, supplies).
///
/// @dev Roles: ADMIN_ROLE (config + sweep), OPERATION_ROLE (per-pool settle/
/// cancel/reschedule). The Pool trusts this factory via onlyFactory.
contract PoolFactory is AccessControl {
using SafeERC20 for IERC20;
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant OPERATION_ROLE = keccak256("OPERATION_ROLE");
/// @notice Sanity ceiling on the pledging window (admin-set duration).
uint256 public constant MAX_DURATION = 30 days;
struct Config {
address virtualToken; // $VIRTUAL
uint256 reserve; // raise target / prebuy, e.g. 50_000e18
uint256 maxContribution; // per-wallet cap, e.g. 500e18
uint256 minContribution; // minimum per-pledge, anti dust-spam, e.g. 1e18
address feeRecipient; // receives the create fee
uint256 feeAmount; // create fee in $VIRT (0 = free)
uint256 duration; // pledging window length (deploy sets 5 days; admin-set via setConfig, bounded by MAX_DURATION)
uint256 tokenTotalSupply; // launched-token total supply, e.g. 1_000_000_000e18
uint256 tokenLpSupply; // LP float (rest is distributable to the pool)
}
Config public config;
mapping(uint256 => address) public pools;
uint256 public poolCount;
bool public createPaused; // admin kill-switch for the permissionless createPool
event ConfigUpdated();
event PoolCreated(uint256 indexed id, address indexed pool, address indexed devRecipient, string ticker);
event CreatePausedSet(bool paused);
/// @param admin DEFAULT_ADMIN + ADMIN_ROLE (config / sweep / role mgmt) —
/// set to a multisig/timelock in prod.
/// @param operator OPERATION_ROLE (per-pool settle/release/claim/cancel) — the
/// hot operator wallet. Kept off the admin role so a hot-key
/// compromise can't touch config, roles, or sweep.
constructor(Config memory c, address admin, address operator) {
require(admin != address(0) && operator != address(0), "zero role");
_setConfig(c);
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(ADMIN_ROLE, admin);
_grantRole(OPERATION_ROLE, operator);
}
/// @notice Pause/unpause permissionless pool creation (defense-in-depth; the
/// factory is directly callable, bypassing any off-chain gate).
function setCreatePaused(bool paused) external onlyRole(ADMIN_ROLE) {
createPaused = paused;
emit CreatePausedSet(paused);
}
function setConfig(Config calldata c) external onlyRole(ADMIN_ROLE) {
_setConfig(c);
}
function _setConfig(Config memory c) internal {
require(c.virtualToken != address(0) && c.feeRecipient != address(0), "zero addr");
require(c.reserve > 0 && c.maxContribution > 0, "bad amount");
// Per-wallet cap must be below the target so no single wallet can fill a
// pool alone (decentralization; >= reserve/cap distinct pledgers to fill).
require(c.maxContribution < c.reserve, "cap >= reserve");
require(c.minContribution > 0 && c.minContribution <= c.maxContribution, "bad min");
// feeAmount == 0 is allowed (free pool creation): createPool skips the
// transfer, and pool spam is bounded off-chain (API rate limits) + by
// createPaused. A fee can be reinstated any time via setConfig.
require(c.duration > 0 && c.duration <= MAX_DURATION, "bad duration");
require(c.tokenLpSupply > 0 && c.tokenTotalSupply >= c.tokenLpSupply, "bad supply");
config = c;
emit ConfigUpdated();
}
/// @notice Permissionless: open a pool for any dev. Charges the create fee
/// if one is configured (feeAmount > 0; 0 = free).
function createPool(PoolCreationParams calldata p) external returns (address) {
require(!createPaused, "create paused");
require(p.devRecipient != address(0), "zero dev");
require(bytes(p.name).length > 0 && bytes(p.ticker).length > 0, "bad meta");
Config memory c = config;
if (c.feeAmount > 0) {
IERC20(c.virtualToken).safeTransferFrom(msg.sender, c.feeRecipient, c.feeAmount);
}
uint256 start = p.startTime > block.timestamp ? p.startTime : block.timestamp;
uint256 id = ++poolCount;
PoolInitParams memory ip = PoolInitParams({
poolId: id,
factory: address(this),
virtualToken: c.virtualToken,
name: p.name,
ticker: p.ticker,
devRecipient: p.devRecipient,
startTime: start,
endTime: start + c.duration,
reserveAmount: c.reserve,
maxContribution: c.maxContribution,
minContribution: c.minContribution,
tokenTotalSupply: c.tokenTotalSupply,
tokenLpSupply: c.tokenLpSupply
});
address pool = address(new Pool(ip));
pools[id] = pool;
emit PoolCreated(id, pool, p.devRecipient, p.ticker);
return pool;
}
// ---- operator surface (forwards to the pool, role-gated) ----
function settleSuccess(uint256 id, address launchedToken, PoolSettlement calldata s) external onlyRole(OPERATION_ROLE) {
Pool(_pool(id)).settleSuccess(launchedToken, s);
}
/// @notice Release $VIRT (<= reserve) from a filled pool to fund the off-chain prebuy.
function releaseForLaunch(uint256 id, address to, uint256 amount) external onlyRole(OPERATION_ROLE) {
Pool(_pool(id)).releaseForLaunch(to, amount);
}
/// @notice Claim a pool's pledger allocation from the TokenTable unlocker into the pool.
function claimUnlocked(uint256 id, address unlocker, uint256[] calldata actualIds, uint256 batchId)
external
onlyRole(OPERATION_ROLE)
{
Pool(_pool(id)).claimUnlocked(unlocker, actualIds, batchId);
}
function settleFailed(uint256 id, uint256[] calldata participantIndexes) external onlyRole(OPERATION_ROLE) {
Pool(_pool(id)).settleFailed(participantIndexes);
}
function cancelPool(uint256 id) external onlyRole(OPERATION_ROLE) {
Pool(_pool(id)).cancel();
}
function resetWindow(uint256 id, uint256 newStart, uint256 newEnd) external onlyRole(OPERATION_ROLE) {
Pool(_pool(id)).resetWindow(newStart, newEnd);
}
/// @notice Move the dev's allocation to the vesting distributor / recover dust.
function sweep(uint256 id, address to, address tkn, uint256 amount) external onlyRole(ADMIN_ROLE) {
Pool(_pool(id)).sweep(to, tkn, amount);
}
function _pool(uint256 id) internal view returns (address) {
address a = pools[id];
require(a != address(0), "no pool");
return a;
}
}
Chain explorer3421msChain node89ms