PGM Raise Contract

BUGBOUNTY! Please visit my Discord to contact me if you find any critical holes in the contract. Only real devs please. I know exactly what AIs think to "see".

The full source code of the PGM Raise Contract. This is the exact code that will be deployed and verified on-chain.

PGMToken is a standard ERC20 ("PenguMiner", "PGM") with a single minter address that can be permanently renounced. Once renounced, no new PGM can ever be minted.

PGMRaise is the raise and price-floor contract. Investors deposit USDT and receive PGM at $0.001. After launch, finalize() creates the CL pool plus two LP NFTs: a floorProtectionNFT (USDT-only, 5% of reserve, activates if price drops to floor) and a priceDiscoveryNFT (PGM-only, sells into upside as price rises). A buyback reserve buys PGM from the market and burns it (reducing total supply). Investors can also redeem PGM directly at the contract for exactly $0.001 per PGM — no DEX swap, no slippage, no fees. This direct redemption is the hard floor guarantee. LP rounding protection (0.01 USDT) ensures the last refunder is never short-changed. Admin functions are gated behind the PGMMultisig (see below). The setupInfo() view returns all contract state in one call for dashboards and AI auditors.

PGMMultisig is a lightweight multisig with a 24-hour timelock. The proposer creates proposals, approvers vote, and execution requires both enough approvals AND 24 hours to have passed since creation. This gives the community time to review any admin action before it takes effect. All investor-critical functions (buyback, redeem, refund, claim, launch) are public and do not require multisig approval.


PGMRaise (with PGMToken)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

interface IERC721Receiver {
    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4);
}

interface INonfungiblePositionManager {
    struct MintParams {
        address token0;
        address token1;
        int24 tickSpacing;
        int24 tickLower;
        int24 tickUpper;
        uint256 amount0Desired;
        uint256 amount1Desired;
        uint256 amount0Min;
        uint256 amount1Min;
        address recipient;
        uint256 deadline;
        uint160 sqrtPriceX96;
    }

    function mint(MintParams calldata params)
        external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);

    struct IncreaseLiquidityParams {
        uint256 tokenId;
        uint256 amount0Desired;
        uint256 amount1Desired;
        uint256 amount0Min;
        uint256 amount1Min;
        uint256 deadline;
    }

    function increaseLiquidity(IncreaseLiquidityParams calldata params)
        external returns (uint128 liquidity, uint256 amount0, uint256 amount1);

    struct DecreaseLiquidityParams {
        uint256 tokenId;
        uint128 liquidity;
        uint256 amount0Min;
        uint256 amount1Min;
        uint256 deadline;
    }

    function decreaseLiquidity(DecreaseLiquidityParams calldata params)
        external returns (uint256 amount0, uint256 amount1);

    struct CollectParams {
        uint256 tokenId;
        address recipient;
        uint128 amount0Max;
        uint128 amount1Max;
    }

    function collect(CollectParams calldata params)
        external returns (uint256 amount0, uint256 amount1);

    function positions(uint256 tokenId) external view returns (
        uint96 nonce, address operator, address token0, address token1,
        int24 tickSpacing, int24 tickLower, int24 tickUpper, uint128 liquidity,
        uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,
        uint128 tokensOwed0, uint128 tokensOwed1
    );
}

interface ISwapRouter {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        int24 tickSpacing;
        address recipient;
        uint256 deadline;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }

    function exactInputSingle(ExactInputSingleParams calldata params)
        external payable returns (uint256 amountOut);
}

interface ICLFactory {
    function getPool(address tokenA, address tokenB, int24 tickSpacing) external view returns (address);
    function createPool(address tokenA, address tokenB, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address pool);
}

interface IPool {
    function slot0() external view returns (
        uint160 sqrtPriceX96, int24 tick, uint16 observationIndex,
        uint16 observationCardinality, uint16 observationCardinalityNext, bool unlocked
    );
}

interface IHermanToken {
    function mint(address to, uint256 amount) external;
    function burn(uint256 amount) external;
    function renounceMinter() external;
}

contract HermanToken is ERC20 {
    address public minter;

    event MinterRenounced();

    constructor(address _minter) ERC20("Herman", "HMAN") {
        minter = _minter;
    }

    function mint(address to, uint256 amount) external {
        require(msg.sender == minter, "not minter");
        _mint(to, amount);
    }

    function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }

    function renounceMinter() external {
        require(msg.sender == minter, "not minter");
        minter = address(0);
        emit MinterRenounced();
    }
}

contract HermanRaise is IERC721Receiver {

    uint256 private _locked = 1;
    modifier nonReentrant() {
        require(_locked == 1, "reentrant");
        _locked = 2;
        _;
        _locked = 1;
    }

    address public constant POSITION_MANAGER = 0xa4890B89dC628baE614780079ACc951Fb0ECdC5F;
    address public constant SWAP_ROUTER = 0xAda5d0E79681038A9547fe6a59f1413F3E720839;
    address public constant CL_FACTORY = 0x8cfE21F272FdFDdf42851f6282c0f998756eEf27;
    address public constant USDT = 0x0709F39376dEEe2A2dfC94A58EdEb2Eb9DF012bD;

    INonfungiblePositionManager public constant posMgr = INonfungiblePositionManager(POSITION_MANAGER);
    ISwapRouter public constant router = ISwapRouter(SWAP_ROUTER);
    ICLFactory public constant clFactory = ICLFactory(CL_FACTORY);

    address public immutable hman;

    int24 public constant TICK_SPACING = 200;
    int24 public constant TICK_START_PRICE = 345400;
    int24 public constant TICK_MAX = 887200;
    int24 public constant TICK_MIN = -887200;
    uint160 public constant SQRT_PRICE_AT_START = 2504784100835956094001232597242347520;

    uint256 public constant SETUP_USDT_FOR_TICK_SWAP = 10000;
    // NOTE: LP rounding protection ensures last refunder gets full amount when downside LP is dissolved
    uint256 public constant LP_ROUNDING_PROTECTION_USDT = 10000;
    uint256 public constant INITIAL_USDT = SETUP_USDT_FOR_TICK_SWAP + LP_ROUNDING_PROTECTION_USDT;

    uint256 public constant LAUNCH_TIME = 1774831405;
    uint256 public constant EMERGENCY_DEADLINE = LAUNCH_TIME + 30 days;

    address public pool;
    uint256 public floorProtectionNFTId;
    uint256 public priceDiscoveryNFTId;

    bool public launched;
    bool public minterRenounced;
    bool public refundMode;
    uint256 public buybackReserve;
    uint256 public refundReserve;
    uint256 public initialUSDTBalance;

    uint256 public totalRaisedUSDT;
    uint256 public totalHMANAllocated;
    uint256 public totalHMANSacrificed;
    uint256 public totalHMANMinted;
    uint256 public totalHMANBurned;
    uint256 public priceDiscoveryHMANAmount;
    uint256 public totalFloorProtectionUSDT;

    mapping(address => uint256) public invested;
    mapping(address => uint256) public allocation;
    mapping(address => uint256) public sacrificed;
    mapping(address => bool) public claimed;

    address public admin;
    address public gameShopContract;
    address public hmanLiquidityContract;

    event Deposited(address indexed investor, uint256 usdt, uint256 hman);
    event Sacrificed(address indexed investor, uint256 hman);
    event Claimed(address indexed investor, uint256 hman);
    event Buyback(address indexed triggerer, uint256 usdtIn, uint256 hmanBurned);
    event Launched();
    event Finalized(uint256 investorHMAN, uint256 priceDiscoveryHMAN);
    event RefundModeActivated(uint256 usdtInReserve, uint256 hmanBurned);
    event Refunded(address indexed user, uint256 hmanIn, uint256 usdtOut);
    event Redeemed(address indexed user, uint256 hmanIn, uint256 usdtOut);

    constructor() {
        admin = msg.sender;
        hman = address(new HermanToken(address(this)));
    }

    function fundPoolSetup_USDT_for_tick_positioning() external {
        require(initialUSDTBalance == 0, "already funded");
        require(IERC20(USDT).transferFrom(msg.sender, address(this), INITIAL_USDT), "send USDT");
        initialUSDTBalance = INITIAL_USDT;
    }

    modifier onlyAdmin() {
        require(msg.sender == admin, "not admin");
        _;
    }

    function onERC721Received(address, address, uint256, bytes calldata)
        external override returns (bytes4)
    {
        return IERC721Receiver.onERC721Received.selector;
    }

    function deposit(uint256 amount) external nonReentrant {
        require(!launched, "launched");
        require(amount >= 1000, "min 0.001 USDT");
        require(IERC20(USDT).transferFrom(msg.sender, address(this), amount), "transfer failed");

        uint256 hmanAmount = amount * 1e15;

        invested[msg.sender] += amount;
        allocation[msg.sender] += hmanAmount;
        totalRaisedUSDT += amount;
        totalHMANAllocated += hmanAmount;
        buybackReserve += amount;

        emit Deposited(msg.sender, amount, hmanAmount);
    }

    function sacrificeAllocation(uint256 hmanAmount) external {
        require(!launched, "launched");
        require(allocation[msg.sender] - sacrificed[msg.sender] >= hmanAmount, "too much");
        sacrificed[msg.sender] += hmanAmount;
        totalHMANSacrificed += hmanAmount;

        uint256 usdtFreed = hmanAmount / 1e15;
        if (usdtFreed > 0) {
            require(gameShopContract != address(0), "set gameShopContract");
            require(usdtFreed <= buybackReserve, "not enough reserve");
            buybackReserve -= usdtFreed;
            require(IERC20(USDT).transfer(gameShopContract, usdtFreed), "transfer failed");
        }

        emit Sacrificed(msg.sender, hmanAmount);
    }

    function launch() external {
        require(!launched, "already launched");
        require(block.timestamp >= LAUNCH_TIME, "too early");
        launched = true;
        emit Launched();
    }

    function finalize() external nonReentrant {
        require(launched, "not launched");
        require(!minterRenounced, "already finalized");
        require(buybackReserve > 0, "nothing raised");
        require(initialUSDTBalance > 0, "call fundPoolSetup_USDT_for_tick_positioning first");

        uint256 effectiveHMAN = totalHMANAllocated - totalHMANSacrificed;
        uint256 priceDiscoveryHMAN = effectiveHMAN / 9;
        priceDiscoveryHMANAmount = priceDiscoveryHMAN;

        uint256 totalToMint = effectiveHMAN + priceDiscoveryHMAN;
        IHermanToken(hman).mint(address(this), totalToMint);
        totalHMANMinted = totalToMint;

        bool usdtIsToken0 = uint160(USDT) < uint160(hman);
        address token0 = usdtIsToken0 ? USDT : hman;
        address token1 = usdtIsToken0 ? hman : USDT;

        pool = clFactory.createPool(token0, token1, TICK_SPACING, SQRT_PRICE_AT_START);

        // NOTE: priceDiscoveryNFT is 100% HMAN, positioned below start price (out of range at creation)
        IERC20(hman).approve(POSITION_MANAGER, priceDiscoveryHMAN);
        {
            int24 upsideTickLower = usdtIsToken0 ? TICK_MIN : TICK_START_PRICE;
            int24 upsideTickUpper = usdtIsToken0 ? TICK_START_PRICE : TICK_MAX;
            uint256 uAmt0 = usdtIsToken0 ? 0 : priceDiscoveryHMAN;
            uint256 uAmt1 = usdtIsToken0 ? priceDiscoveryHMAN : 0;

            (uint256 upId,,,) = posMgr.mint(
                INonfungiblePositionManager.MintParams({
                    token0: token0,
                    token1: token1,
                    tickSpacing: TICK_SPACING,
                    tickLower: upsideTickLower,
                    tickUpper: upsideTickUpper,
                    amount0Desired: uAmt0,
                    amount1Desired: uAmt1,
                    amount0Min: 0,
                    amount1Min: 0,
                    recipient: address(this),
                    deadline: block.timestamp + 600,
                    sqrtPriceX96: 0
                })
            );
            priceDiscoveryNFTId = upId;
        }
        IERC20(hman).approve(POSITION_MANAGER, 0);

        // NOTE: Mini-swap moves tick below start price so floorProtectionNFT deposit is 100% USDT
        IERC20(USDT).approve(SWAP_ROUTER, SETUP_USDT_FOR_TICK_SWAP);
        uint256 hmanBought = router.exactInputSingle(
            ISwapRouter.ExactInputSingleParams({
                tokenIn: USDT,
                tokenOut: hman,
                tickSpacing: TICK_SPACING,
                recipient: address(this),
                deadline: block.timestamp + 600,
                amountIn: SETUP_USDT_FOR_TICK_SWAP,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            })
        );
        IERC20(USDT).approve(SWAP_ROUTER, 0);

        IHermanToken(hman).burn(hmanBought);

        buybackReserve += LP_ROUNDING_PROTECTION_USDT;
        initialUSDTBalance = 0;

        // NOTE: floorProtectionNFT gets 5% of reserve as USDT, positioned above start price (out of range)
        uint256 toDownside = (buybackReserve * 5) / 100;
        buybackReserve -= toDownside;
        {
            int24 downTickLower = usdtIsToken0 ? TICK_START_PRICE : TICK_MIN;
            int24 downTickUpper = usdtIsToken0 ? TICK_MAX : TICK_START_PRICE;
            uint256 dAmt0 = usdtIsToken0 ? toDownside : 0;
            uint256 dAmt1 = usdtIsToken0 ? 0 : toDownside;

            IERC20(USDT).approve(POSITION_MANAGER, toDownside);
            (uint256 downId,,,) = posMgr.mint(
                INonfungiblePositionManager.MintParams({
                    token0: token0,
                    token1: token1,
                    tickSpacing: TICK_SPACING,
                    tickLower: downTickLower,
                    tickUpper: downTickUpper,
                    amount0Desired: dAmt0,
                    amount1Desired: dAmt1,
                    amount0Min: 0,
                    amount1Min: 0,
                    recipient: address(this),
                    deadline: block.timestamp + 600,
                    sqrtPriceX96: 0
                })
            );
            floorProtectionNFTId = downId;
            IERC20(USDT).approve(POSITION_MANAGER, 0);
        }
        totalFloorProtectionUSDT = toDownside;

        IHermanToken(hman).renounceMinter();
        minterRenounced = true;

        emit Finalized(effectiveHMAN, priceDiscoveryHMAN);
    }

    function claim() external nonReentrant {
        require(launched, "not launched");
        require(minterRenounced, "not finalized");
        require(!claimed[msg.sender], "already claimed");

        uint256 amount = allocation[msg.sender] - sacrificed[msg.sender];
        require(amount > 0, "nothing");

        claimed[msg.sender] = true;
        require(IERC20(hman).transfer(msg.sender, amount), "transfer failed");
        emit Claimed(msg.sender, amount);
    }

    // NOTE: redeem makes no sense if token trades above $0.001 — direct 1:1 at floor price
    function redeem(uint256 hmanAmount) external nonReentrant {
        require(launched, "not launched");
        require(hmanAmount > 0, "zero");

        uint256 usdtOut = hmanAmount / 1e15;
        require(usdtOut > 0, "too small");
        require(usdtOut <= buybackReserve, "exceeds reserve");

        require(IERC20(hman).transferFrom(msg.sender, address(this), hmanAmount), "transfer failed");
        IHermanToken(hman).burn(hmanAmount);
        totalHMANBurned += hmanAmount;
        buybackReserve -= usdtOut;

        require(IERC20(USDT).transfer(msg.sender, usdtOut), "transfer failed");
        emit Redeemed(msg.sender, hmanAmount, usdtOut);
    }

    function triggerBuyback(uint256 usdtAmount) external nonReentrant {
        require(launched, "not launched");
        require(usdtAmount > 0, "zero amount");
        require(usdtAmount <= buybackReserve, "exceeds reserve");

        if (buybackReserve < 1e6 && !refundMode) {
            _activateRefundMode();
            return;
        }

        uint256 minHMANOut = usdtAmount * 1e15;
        buybackReserve -= usdtAmount;

        IERC20(USDT).approve(SWAP_ROUTER, usdtAmount);
        uint256 hmanBought = router.exactInputSingle(
            ISwapRouter.ExactInputSingleParams({
                tokenIn: USDT,
                tokenOut: hman,
                tickSpacing: TICK_SPACING,
                recipient: address(this),
                deadline: block.timestamp + 600,
                amountIn: usdtAmount,
                amountOutMinimum: minHMANOut,
                sqrtPriceLimitX96: 0
            })
        );
        IERC20(USDT).approve(SWAP_ROUTER, 0);

        IHermanToken(hman).burn(hmanBought);
        totalHMANBurned += hmanBought;
        emit Buyback(msg.sender, usdtAmount, hmanBought);
    }

    function activateRefundMode() external nonReentrant {
        require(launched, "not launched");
        require(!refundMode, "already active");
        require(buybackReserve < 1e6, "reserve not empty");
        _activateRefundMode();
    }

    function _activateRefundMode() internal {
        require(floorProtectionNFTId != 0, "no floor protection NFT");
        (,,,,,,, uint128 liq,,,,) = posMgr.positions(floorProtectionNFTId);
        require(liq > 0, "NFT empty");

        posMgr.decreaseLiquidity(INonfungiblePositionManager.DecreaseLiquidityParams({
            tokenId: floorProtectionNFTId,
            liquidity: liq,
            amount0Min: 0,
            amount1Min: 0,
            deadline: block.timestamp + 600
        }));

        (uint256 collected0, uint256 collected1) = posMgr.collect(
            INonfungiblePositionManager.CollectParams({
                tokenId: floorProtectionNFTId,
                recipient: address(this),
                amount0Max: type(uint128).max,
                amount1Max: type(uint128).max
            })
        );

        bool usdtIsToken0 = uint160(USDT) < uint160(hman);
        uint256 usdtCollected;
        uint256 hmanCollected;
        if (usdtIsToken0) {
            usdtCollected = collected0;
            hmanCollected = collected1;
        } else {
            usdtCollected = collected1;
            hmanCollected = collected0;
        }

        if (hmanCollected > 0) {
            IHermanToken(hman).burn(hmanCollected);
            totalHMANBurned += hmanCollected;
        }

        refundReserve = usdtCollected;
        totalFloorProtectionUSDT = 0;
        refundMode = true;
        emit RefundModeActivated(usdtCollected, hmanCollected);
    }

    function refund(uint256 hmanAmount) external nonReentrant {
        require(refundMode, "refund not active");
        require(hmanAmount > 0, "zero amount");

        uint256 usdtOut = hmanAmount / 1e15;
        require(usdtOut > 0, "amount too small");
        require(usdtOut <= refundReserve, "exceeds refund reserve");

        require(IERC20(hman).transferFrom(msg.sender, address(this), hmanAmount), "transfer failed");
        IHermanToken(hman).burn(hmanAmount);
        totalHMANBurned += hmanAmount;

        refundReserve -= usdtOut;
        require(IERC20(USDT).transfer(msg.sender, usdtOut), "transfer failed");
        emit Refunded(msg.sender, hmanAmount, usdtOut);
    }

    function withdrawExcessUSDT(address to) external nonReentrant onlyAdmin {
        require(launched, "not launched");
        require(to != address(0), "zero address");
        require(_currentTickAboveFloor(), "price below floor");

        uint256 investorMinted = totalHMANMinted - priceDiscoveryHMANAmount;
        uint256 investorHMANCirculating = totalHMANBurned >= investorMinted ? 0 : investorMinted - totalHMANBurned;
        uint256 usdtNeeded = investorHMANCirculating / 1e15;

        uint256 usdtFree = buybackReserve + refundReserve;
        uint256 totalBacking = usdtFree + totalFloorProtectionUSDT;
        require(totalBacking > usdtNeeded, "no excess");

        uint256 excess = totalBacking - usdtNeeded;
        if (excess > usdtFree) {
            excess = usdtFree;
        }
        require(excess > 0, "no withdrawable excess");

        if (excess <= buybackReserve) {
            buybackReserve -= excess;
        } else {
            uint256 remaining = excess - buybackReserve;
            buybackReserve = 0;
            refundReserve -= remaining;
        }

        require(IERC20(USDT).transfer(to, excess), "transfer failed");
    }

    function collectFloorProtectionFees(address feeRecipient) external onlyAdmin {
        require(feeRecipient != address(0), "zero address");
        require(floorProtectionNFTId != 0 && !refundMode, "no active NFT");
        posMgr.collect(INonfungiblePositionManager.CollectParams({
            tokenId: floorProtectionNFTId,
            recipient: feeRecipient,
            amount0Max: type(uint128).max,
            amount1Max: type(uint128).max
        }));
    }

    function collectPriceDiscoveryFees(address feeRecipient) external onlyAdmin {
        require(feeRecipient != address(0), "zero address");
        require(priceDiscoveryNFTId != 0, "no price discovery NFT");
        posMgr.collect(INonfungiblePositionManager.CollectParams({
            tokenId: priceDiscoveryNFTId,
            recipient: feeRecipient,
            amount0Max: type(uint128).max,
            amount1Max: type(uint128).max
        }));
    }

    function _currentTickAboveFloor() internal view returns (bool) {
        if (refundMode) return true;
        if (pool == address(0)) return false;
        (, int24 currentTick,,,,) = IPool(pool).slot0();
        bool usdtIsToken0 = uint160(USDT) < uint160(hman);
        if (usdtIsToken0) {
            return currentTick <= TICK_START_PRICE;
        } else {
            return currentTick >= TICK_START_PRICE;
        }
    }

    function emergencyWithdraw() external nonReentrant {
        require(launched, "not launched");
        require(!minterRenounced, "already finalized");
        require(block.timestamp >= EMERGENCY_DEADLINE, "too early");

        uint256 sacUSDT = sacrificed[msg.sender] / 1e15;
        uint256 usdtToReturn = invested[msg.sender] > sacUSDT ? invested[msg.sender] - sacUSDT : 0;
        require(usdtToReturn > 0, "nothing to withdraw");

        uint256 alloc = allocation[msg.sender];
        uint256 sac = sacrificed[msg.sender];

        invested[msg.sender] = 0;
        allocation[msg.sender] = 0;
        sacrificed[msg.sender] = 0;

        totalRaisedUSDT -= (alloc / 1e15);
        totalHMANAllocated -= alloc;
        totalHMANSacrificed -= sac;

        require(usdtToReturn <= buybackReserve, "insufficient reserve");
        buybackReserve -= usdtToReturn;

        require(IERC20(USDT).transfer(msg.sender, usdtToReturn), "transfer failed");
    }

    function setGameShopContract(address _addr) external onlyAdmin {
        require(_addr != address(0), "zero address");
        gameShopContract = _addr;
    }

    function setHmanLiquidityContract(address _addr) external onlyAdmin {
        require(_addr != address(0), "zero address");
        hmanLiquidityContract = _addr;
    }

    function claimable(address investor) external view returns (uint256) {
        if (!launched || !minterRenounced || claimed[investor]) return 0;
        return allocation[investor] - sacrificed[investor];
    }

    function projectedMaxSupply() external view returns (uint256) {
        uint256 eff = totalHMANAllocated - totalHMANSacrificed;
        return eff + (eff / 9);
    }

    function stats() external view returns (
        uint256 raised, uint256 allocated, uint256 sacr, uint256 reserve, bool live
    ) {
        return (totalRaisedUSDT, totalHMANAllocated, totalHMANSacrificed, buybackReserve, launched);
    }

    function setupInfo() external view returns (
        address _pool,
        address _hmanToken,
        address _usdtToken,
        uint256 _floorProtectionNFTId,
        uint256 _priceDiscoveryNFTId,
        int24 _tickSpacing,
        int24 _tickStartPrice,
        address _gameShopContract,
        address _hmanLiquidityContract,
        address _admin,
        bool _isLaunched,
        bool _isFinalized,
        bool _isRefundMode,
        uint256 _buybackReserve,
        uint256 _refundReserve,
        uint256 _totalFloorProtectionUSDT,
        uint256 _totalHMANMinted,
        uint256 _totalHMANBurned,
        uint256 _priceDiscoveryHMANAmount
    ) {
        return (
            pool,
            hman,
            USDT,
            floorProtectionNFTId,
            priceDiscoveryNFTId,
            TICK_SPACING,
            TICK_START_PRICE,
            gameShopContract,
            hmanLiquidityContract,
            admin,
            launched,
            minterRenounced,
            refundMode,
            buybackReserve,
            refundReserve,
            totalFloorProtectionUSDT,
            totalHMANMinted,
            totalHMANBurned,
            priceDiscoveryHMANAmount
        );
    }
}

PGMMultisig

A lightweight multisig that gates all admin functions on PGMRaise. The proposer (dev) creates proposals, approvers (community/investors) vote, and execution requires both enough approvals AND a 24-hour timelock since proposal creation. This ensures the community can review any admin action before it takes effect. If the multisig gets stuck (approvers unresponsive), no investor funds are at risk — all critical functions (buyback, redeem, refund, claim, launch) are public and permissionless.

Last updated

Was this helpful?