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
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