PGM Raise Contract

This is the complete source code of the PGM Raise Contract (PGMRaise). For the mathematical solvency proof, see Mathematical Solvency Proof.

Live deployment (Abstract Mainnet)

Address
PGMRaise (the contract below) 0x988Fd61C834B182357E3dAdf358DE9bF0A7De336
PGM Token (ERC-20, minter renounced after finalize()) 0xB4e36EF1d2a459b0f40A6618f4179a992ff0B96E
Pool (Aborean Slipstream PGM/USDT, tick-spacing 200) 0x5D6504B29Bc6EF2d496898bD3E97CE743Ef7dD66

Parameters baked into deployment:

The source verified on Abscan exactly matches the code below — anyone can re-compile from this whitepaper, hash, and compare. The runMechanicsSmokeTest() was executed against the live pool right after deployment and passed; deposits are open from that block onwards.

Source

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

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

interface ISacrificeProxy {
    function deposit(address user, uint256 pgmAmount, uint256 usdtAmount) external;
}

interface ITradingFeeProxy {
    function deposit(uint256 usdtAmount, uint256 pgmAmount) external;
}

interface IExcessUSDTProxy {
    function deposit(uint256 usdtAmount) external;
}

interface IReferralRegistry {
    function registerIfNew(address user, address referrer) external returns (address actualReferrer, bool wasNew);
}

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 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 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
    );
    function swap(address recipient, bool zeroForOne, int256 amountSpecified,
        uint160 sqrtPriceLimitX96, bytes calldata data) external returns (int256 amount0, int256 amount1);
}

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

contract PGMToken is ERC20 {
    address public minter;
    event MinterRenounced();

    constructor(address _minter, string memory _name, string memory _symbol) ERC20(_name, _symbol) {
        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 PGMRaise is IERC721Receiver {
    using SafeERC20 for IERC20;

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

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

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

    address public immutable pgm;

    int24 public constant TICK_SPACING = 200;
    int24 public constant TICK_START_PRICE = 345400;       // ~$0.001 (floor)
    int24 public constant TICK_UPSIDE_START = 345200;      // one tick above floor (gap!)
    int24 public constant TICK_MAX = 887200;
    int24 public constant TICK_MIN = -887200;
    uint160 public constant SQRT_PRICE_AT_START = 2504784100835956094001232597242347520;

    uint256 public constant PGM_PER_USDT = 1e15;

    uint256 public immutable LAUNCH_TIME;
    uint256 public immutable EMERGENCY_DEADLINE;

    address public pool;
    uint256 public priceDiscoveryNFTId;

    bool public smokeTestPassed;
    bool public launched;
    bool public minterRenounced;
    uint256 public buybackReserve;

    uint256 public totalRaisedUSDT;
    uint256 public totalPGMAllocated;
    uint256 public totalPGMSacrificed;
    uint256 public totalPGMMinted;
    uint256 public totalPGMBurned;
    uint256 public priceDiscoveryPGMAmount;

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

    address public immutable shopTreasuryProxy;
    address public immutable gameTreasuryProxy;
    address public immutable stakingTreasuryProxy;
    address public immutable badgeTreasuryProxy;
    address public immutable tradingFeeProxy;
    address public immutable excessUSDTProxy;
    address public immutable referralRegistry;

    uint256 public pendingExcess;

    bool private _inSwap;

    event Deposited(address indexed investor, uint256 usdt, uint256 pgm);
    event Sacrificed(address indexed investor, uint256 pgm);
    event Claimed(address indexed investor, uint256 pgm);
    event Launched();
    event Finalized(uint256 investorPGM, uint256 priceDiscoveryPGM);
    event Redeemed(address indexed user, uint256 pgmIn, uint256 usdtOut);
    event ExcessCollected(uint256 excess);
    event SmokeTestPassed();
    event TradingFeesCollected(uint256 usdt, uint256 pgm);
    event EmergencyWithdrawn(address indexed user, uint256 usdtReturned);

    constructor(
        uint256 _launchTime,
        uint256 _emergencyDeadline,
        string memory _tokenName,
        string memory _tokenSymbol,
        address _shopTreasuryProxy,
        address _gameTreasuryProxy,
        address _stakingTreasuryProxy,
        address _badgeTreasuryProxy,
        address _tradingFeeProxy,
        address _excessUSDTProxy,
        address _referralRegistry
    ) {
        require(_launchTime > block.timestamp, "launch must be future");
        require(_emergencyDeadline > _launchTime, "deadline must be after launch");
        require(_shopTreasuryProxy != address(0), "shopTreasuryProxy=0");
        require(_gameTreasuryProxy != address(0), "gameTreasuryProxy=0");
        require(_stakingTreasuryProxy != address(0), "stakingTreasuryProxy=0");
        require(_badgeTreasuryProxy != address(0), "badgeTreasuryProxy=0");
        require(_tradingFeeProxy != address(0), "tradingFeeProxy=0");
        require(_excessUSDTProxy != address(0), "excessUSDTProxy=0");
        require(_referralRegistry != address(0), "referralRegistry=0");

        LAUNCH_TIME = _launchTime;
        EMERGENCY_DEADLINE = _emergencyDeadline;
        shopTreasuryProxy = _shopTreasuryProxy;
        gameTreasuryProxy = _gameTreasuryProxy;
        stakingTreasuryProxy = _stakingTreasuryProxy;
        badgeTreasuryProxy = _badgeTreasuryProxy;
        tradingFeeProxy = _tradingFeeProxy;
        excessUSDTProxy = _excessUSDTProxy;
        referralRegistry = _referralRegistry;

        pgm = address(new PGMToken(address(this), _tokenName, _tokenSymbol));
        require(uint160(USDT) < uint160(pgm), "token order wrong, redeploy");

        pool = clFactory.createPool(USDT, pgm, TICK_SPACING, SQRT_PRICE_AT_START);
    }

    /// @notice Runs a complete lifecycle test with real USDT and real pool mechanics.
    ///         Tests: pool creation, LP mint, deposit, launch, finalize, claim, redeem (with swap),
    ///         and DEX buy simulation. All inside try/catch so state is reverted.
    ///         Sets smokeTestPassed = true on success. Deposits require this flag.
    ///         Caller must have approved at least 10000 USDT_raw (0.01 USDT) to this contract.
    function runMechanicsSmokeTest() external nonReentrant {
        require(!smokeTestPassed, "already tested");
        require(pool != address(0), "pool not created"); // pool was created in constructor

        IERC20(USDT).safeTransferFrom(msg.sender, address(this), 10000);

        try this._lifecycleTest() {
            revert("lifecycle test should have reverted");
        } catch Error(string memory reason) {
            require(keccak256(bytes(reason)) == keccak256("LIFECYCLE_OK"), reason);
        } catch (bytes memory) {
            revert("smoke test: low-level revert");
        }

        IERC20(USDT).safeTransfer(msg.sender, 10000);

        smokeTestPassed = true;
        emit SmokeTestPassed();
    }

    /// @dev Mechanics smoke test. Called via try/catch — everything reverts.
    ///      Calls the REAL _*Core() functions to test actual business logic.
    ///      We have 10000 USDT_raw (0.01 USDT) from the caller.
    function _lifecycleTest() external {
        require(msg.sender == address(this), "only self");

        // Long-lived locals (used across multiple phases below)
        address W1 = address(uint160(0x1001));
        address W2 = address(uint160(0x1002));
        address W3 = address(uint160(0x1003));
        uint256 usdtInLP = 0; // tracks USDT sitting in the upside LP

        invested[W1] = 5000; allocation[W1] = 5000 * PGM_PER_USDT;
        invested[W2] = 3000; allocation[W2] = 3000 * PGM_PER_USDT;
        invested[W3] = 2000; allocation[W3] = 2000 * PGM_PER_USDT;
        totalRaisedUSDT = 10000;
        totalPGMAllocated = 10000 * PGM_PER_USDT;
        buybackReserve = 10000;

        // Setup-phase sacrifice (locals scoped so they release after assertions)
        {
            (uint256 sacUsdt, uint256 sacPgm) = _sacrificeCore(W3, 1000 * PGM_PER_USDT);
            require(sacUsdt == 1000, "setup: sacrifice usdt wrong");
            require(sacPgm == 1000 * PGM_PER_USDT, "setup: sacrifice pgm wrong");
        }
        require(buybackReserve == 9000, "setup: reserve wrong");

        _launchCore();
        require(launched, "setup: launch failed");
        _finalizeCore();
        require(minterRenounced, "setup: finalize failed");
        require(pool != address(0), "setup: pool not created");
        require(priceDiscoveryNFTId > 0, "setup: no NFT");
        _checkInvariant(usdtInLP, "after finalize");

        for (uint256 i = 1; i <= 5; i++) {
            uint256 amt = i * 1000000;
            require(amt * PGM_PER_USDT / PGM_PER_USDT == amt, "t1: roundtrip");
        }

        // t2: per-wallet alloc — these MUST persist for later claim/redeem phases
        uint256 w1PGM = allocation[W1] - sacrificed[W1];
        uint256 w2PGM = allocation[W2] - sacrificed[W2];
        uint256 w3PGM = allocation[W3] - sacrificed[W3];
        require(w1PGM == 5000 * PGM_PER_USDT, "t2: W1 alloc wrong");
        require(w2PGM == 3000 * PGM_PER_USDT, "t2: W2 alloc wrong");
        require(w3PGM == 1000 * PGM_PER_USDT, "t2: W3 alloc wrong");

        _claimCore(W1, w1PGM, address(this));
        _claimCore(W2, w2PGM, address(this));
        _claimCore(W3, w3PGM, address(this));

        IERC20(pgm).approve(address(this), type(uint256).max);

        // t3: W1 reserve-path redeem.
        // Test calls _redeemCore with user=address(this). Since user==self the final
        // safeTransfer is a self-transfer, but the contract's USDT balance still
        // changes by +usdtFromSwap (swap path delivers USDT into contract), and
        // buybackReserve drops by usdtFromReserve. Sum equals usdtOut for all paths
        // (pure reserve, pure swap, or mixed).
        {
            uint256 usdtBefore3 = IERC20(USDT).balanceOf(address(this));
            uint256 reserveBefore3 = buybackReserve;
            _redeemCore(address(this), w1PGM);
            uint256 fromSwap = IERC20(USDT).balanceOf(address(this)) - usdtBefore3;
            uint256 fromReserve = reserveBefore3 - buybackReserve;
            require(fromSwap + fromReserve == 5000, "t3: W1 did not get 5000 USDT back");
        }
        _checkInvariant(usdtInLP, "after W1 redeem");

        // t4: trader DEX buy — pgmBoughtByTrader persists for t7
        uint256 pgmBoughtByTrader;
        {
            uint256 usdtForBuy = 2000;
            _inSwap = true;
            (int256 b0, int256 b1) = IPool(pool).swap(
                address(this), true, int256(usdtForBuy), SQRT_PRICE_DEX_BUY_LIMIT, ""
            );
            _inSwap = false;
            require(b0 > 0 && b1 < 0, "t4: buy failed");
            pgmBoughtByTrader = uint256(-b1);
            uint256 usdtSpentOnBuy = uint256(b0);
            // Per PROOF Lemma L1 (Buy from locked upside LP):
            //   dR = 0, dK = +x, dC = +x
            // Trader buys from LP do NOT change buybackReserve. The USDT the trader
            // pays goes into the LP (K) and externalises a matching PGM claim (C).
            // R is untouched. Even though here the smoke test uses address(this) as
            // the trader, the proof's accounting still holds: the simulation models
            // a trader paying USDT into the LP, not the reserve being spent.
            //
            // UniV3 pool fees stay inside the pool until collect() is called, so the
            // pool's USDT balance after the buy is the full usdtSpentOnBuy (no fee
            // deduction). For the invariant check here, the LP-backed USDT (= K) is
            // the full amount.
            usdtInLP += usdtSpentOnBuy;
        }
        _checkInvariant(usdtInLP, "after DEX buy");

        // t5: W2 redeem (mixed path: swap covers part, reserve the rest — see t3 note)
        {
            uint256 usdtBefore5 = IERC20(USDT).balanceOf(address(this));
            uint256 reserveBefore5 = buybackReserve;
            _redeemCore(address(this), w2PGM);
            uint256 fromSwap = IERC20(USDT).balanceOf(address(this)) - usdtBefore5;
            uint256 fromReserve = reserveBefore5 - buybackReserve;
            require(fromSwap + fromReserve == 3000, "t5: W2 did not get 3000 USDT back");
        }
        usdtInLP = 0;
        _checkInvariant(usdtInLP, "after W2 redeem");

        // t6: W3 redeem (same combined formula as t3)
        {
            uint256 usdtBefore6 = IERC20(USDT).balanceOf(address(this));
            uint256 reserveBefore6 = buybackReserve;
            _redeemCore(address(this), w3PGM);
            uint256 fromSwap = IERC20(USDT).balanceOf(address(this)) - usdtBefore6;
            uint256 fromReserve = reserveBefore6 - buybackReserve;
            require(fromSwap + fromReserve == 1000, "t6: W3 did not get 1000 USDT back");
        }
        _checkInvariant(usdtInLP, "after W3 redeem");

        // t7: trader-bought PGM redeems at exact floor price (combined formula as t3)
        {
            uint256 traderUsdtExpected = pgmBoughtByTrader / PGM_PER_USDT;
            uint256 usdtBefore7 = IERC20(USDT).balanceOf(address(this));
            uint256 reserveBefore7 = buybackReserve;
            _redeemCore(address(this), pgmBoughtByTrader);
            uint256 fromSwap = IERC20(USDT).balanceOf(address(this)) - usdtBefore7;
            uint256 fromReserve = reserveBefore7 - buybackReserve;
            require(fromSwap + fromReserve == traderUsdtExpected, "t7: trader did not get floor price");
        }
        _checkInvariant(usdtInLP, "after trader redeem");

        // t8: reserve-coverage invariant
        {
            uint256 amountPGMneedToBurnInitially = totalPGMMinted - priceDiscoveryPGMAmount;
            uint256 circulating = amountPGMneedToBurnInitially > totalPGMBurned ? amountPGMneedToBurnInitially - totalPGMBurned : 0;
            uint256 usdtNeeded = (circulating + PGM_PER_USDT - 1) / PGM_PER_USDT;
            require(buybackReserve >= usdtNeeded, "t8: RESERVE UNDERFUNDED - CRITICAL");
        }

        // t10: rounding / ordering checks
        for (uint256 i = 1; i <= 5; i++) {
            uint256 amt = i * 1000000;
            require(amt * PGM_PER_USDT / PGM_PER_USDT == amt, "t10: roundtrip fail");
        }
        require((PGM_PER_USDT - 1) / PGM_PER_USDT == 0, "t10: dust should be 0");
        {
            uint256 wantPGM = PGM_PER_USDT * 1000 + 1;
            uint256 usdtCost = (wantPGM + PGM_PER_USDT - 1) / PGM_PER_USDT;
            require(usdtCost == 1001, "t10: ceiling wrong");
        }
        {
            uint256 redeemWithDust = PGM_PER_USDT + 50;
            require(redeemWithDust - (redeemWithDust / PGM_PER_USDT * PGM_PER_USDT) == 50, "t10: dust calc");
        }
        require(uint160(USDT) < uint160(pgm), "t10: token order wrong");

        // t11: emergency-withdraw math
        {
            uint256 w2SacUSDT = sacrificed[W2] / PGM_PER_USDT;
            uint256 w2EmReturn = invested[W2] > w2SacUSDT ? invested[W2] - w2SacUSDT : 0;
            require(w2EmReturn == 3000, "t11: W2 emergency math wrong");
        }
        {
            uint256 w3SacUSDT = sacrificed[W3] / PGM_PER_USDT;
            uint256 w3EmReturn = invested[W3] > w3SacUSDT ? invested[W3] - w3SacUSDT : 0;
            require(w3EmReturn == 1000, "t11: W3 emergency math wrong");
        }

        revert("LIFECYCLE_OK");
    }

    /// @dev Test-only conservative helper used in smoke/scenario checks.
    ///      This is not the formal proof quantity K and not a live LP-state measurement.
    ///      It approximates R + K >= C using buybackReserve + usdtInLP >= ReserveForNonLPBackedPGMneeded().
    function _checkInvariant(uint256 usdtInLP, string memory step) internal view {
        if (!minterRenounced) return;
        require(buybackReserve + usdtInLP >= ReserveForNonLPBackedPGMneeded(),
            string.concat("INVARIANT VIOLATED at ", step));
    }

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

    /// @notice Uniswap V3 swap callback — called by the pool during swap.
    ///         Handles selling PGM (redeem) and smoke test DEX-buy simulation.
    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {
        if (pool != address(0)) {
            require(msg.sender == pool, "not the pool");
        } else {
            require(msg.sender == clFactory.getPool(USDT, pgm, TICK_SPACING), "not a valid pool");
        }
        require(_inSwap, "unexpected callback");
        if (amount0Delta > 0) {
            IERC20(USDT).safeTransfer(msg.sender, uint256(amount0Delta));
        }
        if (amount1Delta > 0) {
            IERC20(pgm).safeTransfer(msg.sender, uint256(amount1Delta));
        }
    }

    function deposit(uint256 usdtAmount) external nonReentrant {
        _depositChecks();
        _depositCore(msg.sender, msg.sender, usdtAmount, usdtAmount * PGM_PER_USDT);
        try IReferralRegistry(referralRegistry).registerIfNew(msg.sender, address(0)) {} catch {}
    }
    function deposit(uint256 usdtAmount, address referrer) external nonReentrant {
        _depositChecks();
        _depositCore(msg.sender, msg.sender, usdtAmount, usdtAmount * PGM_PER_USDT);
        try IReferralRegistry(referralRegistry).registerIfNew(msg.sender, referrer) {} catch {}
    }

    /// @notice Deposit USDT on behalf of another wallet. USDT comes from msg.sender,
    ///         allocation goes to beneficiary. No referral is registered — the beneficiary
    ///         can set their own referrer by depositing themselves (even a tiny amount).
    function giveDeposit(address beneficiary, uint256 usdtAmount) external nonReentrant {
        _depositChecks();
        require(beneficiary != address(0), "beneficiary=0");
        _depositCore(msg.sender, beneficiary, usdtAmount, usdtAmount * PGM_PER_USDT);
    }

    function _depositChecks() internal view {
        require(smokeTestPassed, "smoke test not run");
        require(!launched, "launched");
        require(block.timestamp < LAUNCH_TIME, "raise ended");
    }
    function _depositCore(address payer, address beneficiary, uint256 usdtAmount, uint256 pgmAmount) internal {
        require(usdtAmount >= 1000, "min 0.001 USDT");
        IERC20(USDT).safeTransferFrom(payer, address(this), usdtAmount);

        invested[beneficiary] += usdtAmount;
        allocation[beneficiary] += pgmAmount;
        totalRaisedUSDT += usdtAmount;
        totalPGMAllocated += pgmAmount;
        buybackReserve += usdtAmount;

        emit Deposited(beneficiary, usdtAmount, pgmAmount);
    }

    function sacrificePGMAllocForShop(uint256 pgmAmount) external nonReentrant {
        _sacrifice(msg.sender, msg.sender, pgmAmount, shopTreasuryProxy);
    }
    function sacrificePGMAllocForGame(uint256 pgmAmount) external nonReentrant {
        _sacrifice(msg.sender, msg.sender, pgmAmount, gameTreasuryProxy);
    }
    function sacrificePGMAllocForStaking(uint256 pgmAmount) external nonReentrant {
        _sacrifice(msg.sender, msg.sender, pgmAmount, stakingTreasuryProxy);
    }
    function sacrificePGMAllocForBadge(uint256 pgmAmount) external nonReentrant {
        _sacrifice(msg.sender, msg.sender, pgmAmount, badgeTreasuryProxy);
    }

    function sacrificePGMAllocForShop(uint256 pgmAmount, address beneficiary) external nonReentrant {
        _sacrifice(msg.sender, beneficiary, pgmAmount, shopTreasuryProxy);
    }
    function sacrificePGMAllocForGame(uint256 pgmAmount, address beneficiary) external nonReentrant {
        _sacrifice(msg.sender, beneficiary, pgmAmount, gameTreasuryProxy);
    }
    function sacrificePGMAllocForStaking(uint256 pgmAmount, address beneficiary) external nonReentrant {
        _sacrifice(msg.sender, beneficiary, pgmAmount, stakingTreasuryProxy);
    }
    function sacrificePGMAllocForBadge(uint256 pgmAmount, address beneficiary) external nonReentrant {
        _sacrifice(msg.sender, beneficiary, pgmAmount, badgeTreasuryProxy);
    }

    function _sacrifice(address sacrificer, address beneficiary, uint256 pgmAmount, address target) internal {
        require(!launched, "launched");
        require(beneficiary != address(0), "beneficiary=0");
        (uint256 usdtFreed, uint256 pgmToSacrifice) = _sacrificeCore(sacrificer, pgmAmount);
        IERC20(USDT).safeTransfer(target, usdtFreed);
        ISacrificeProxy(target).deposit(beneficiary, pgmToSacrifice, usdtFreed);
        emit Sacrificed(sacrificer, pgmToSacrifice);
    }
    function _sacrificeCore(address sacrificer, uint256 pgmAmount) internal returns (uint256 usdtFreed, uint256 pgmToSacrifice) {
        usdtFreed = pgmAmount / PGM_PER_USDT;
        require(usdtFreed > 0, "amount too small");
        pgmToSacrifice = usdtFreed * PGM_PER_USDT;

        require(allocation[sacrificer] - sacrificed[sacrificer] >= pgmToSacrifice, "too much");
        sacrificed[sacrificer] += pgmToSacrifice;
        totalPGMSacrificed += pgmToSacrifice;

        require(usdtFreed <= buybackReserve, "not enough reserve");
        buybackReserve -= usdtFreed;
    }

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

    function finalize() external nonReentrant {
        require(launched, "not launched");
        _finalizeCore();
    }
    function _finalizeCore() internal {
        require(!minterRenounced, "already finalized");
        require(buybackReserve > 0, "nothing raised");

        uint256 effectivePGM = totalPGMAllocated - totalPGMSacrificed;
        uint256 priceDiscoveryPGM = effectivePGM / 9;
        priceDiscoveryPGMAmount = priceDiscoveryPGM;

        uint256 totalToMint = effectivePGM + priceDiscoveryPGM;
        IPGMToken(pgm).mint(address(this), totalToMint);
        totalPGMMinted = totalToMint;

        address token0 = USDT;
        address token1 = pgm;

        require(pool != address(0), "pool not created");

        IERC20(pgm).approve(POSITION_MANAGER, priceDiscoveryPGM);
        {
            (uint256 upId,, uint256 upAmt0, uint256 upAmt1) = posMgr.mint(
                INonfungiblePositionManager.MintParams({
                    token0: token0, token1: token1, tickSpacing: TICK_SPACING,
                    tickLower: TICK_MIN, tickUpper: TICK_UPSIDE_START,
                    amount0Desired: 0, amount1Desired: priceDiscoveryPGM,
                    amount0Min: 0, amount1Min: 0,
                    recipient: address(this), deadline: block.timestamp + 600, sqrtPriceX96: 0
                })
            );
            priceDiscoveryNFTId = upId;
            require(upAmt0 == 0, "upside LP should have 0 USDT");
            require(upAmt1 > 0, "upside LP should have PGM");
        }
        IERC20(pgm).approve(POSITION_MANAGER, 0);

        IPGMToken(pgm).renounceMinter();
        minterRenounced = true;

        emit Finalized(effectivePGM, priceDiscoveryPGM);
    }

    function claim(uint256 amount, address to) external nonReentrant {
        _claimCore(msg.sender, amount, to);
    }
    function _claimCore(address user, uint256 amount, address to) internal {
        require(minterRenounced, "not finalized");
        require(to != address(0), "zero address");

        uint256 maxClaimable = allocation[user] - sacrificed[user] - claimed[user];
        require(amount > 0 && amount <= maxClaimable, "bad amount");

        claimed[user] += amount;
        IERC20(pgm).safeTransfer(to, amount);
        emit Claimed(user, amount);
    }

    function redeem(uint256 pgmAmount) external nonReentrant {
        _redeemCore(msg.sender, pgmAmount);
    }
    function _redeemCore(address user, uint256 pgmAmount) internal {
        require(minterRenounced, "not finalized");
        require(pgmAmount > 0, "zero");

        uint256 usdtOut = pgmAmount / PGM_PER_USDT;
        require(usdtOut > 0, "too small");
        require(IERC20(pgm).balanceOf(user) >= pgmAmount, "insufficient PGM");

        uint256 pgmForFloor = usdtOut * PGM_PER_USDT;
        uint256 dust = pgmAmount - pgmForFloor;

        IERC20(pgm).safeTransferFrom(user, address(this), pgmAmount);

        uint256 usdtFromSwap = 0;
        uint256 pgmSwapped = 0;

        // Defense in depth: Uniswap V3 swap() does not revert on zero liquidity
        // (it returns 0), but we wrap it defensively so redeem() can never be
        // blocked by any external call. Reserve always covers the full floor.
        if (pool != address(0)) {
            (, int24 currentTick,,,,) = IPool(pool).slot0();
            if (currentTick < TICK_START_PRICE) {
                uint256 usdtBefore = IERC20(USDT).balanceOf(address(this));
                uint256 pgmBefore = IERC20(pgm).balanceOf(address(this));

                _inSwap = true;
                try IPool(pool).swap(
                    address(this), false, int256(pgmForFloor),
                    SQRT_PRICE_REDEEM_LIMIT, ""
                ) {} catch {} // never expected: non-revert verified by smoke test against real pool
                _inSwap = false;

                usdtFromSwap = IERC20(USDT).balanceOf(address(this)) - usdtBefore;
                pgmSwapped = pgmBefore - IERC20(pgm).balanceOf(address(this));
            }
        }

        uint256 pgmUnswapped = pgmForFloor - pgmSwapped;
        if (pgmUnswapped == 0) {
            require(usdtFromSwap >= usdtOut, "lp swap below floor");
        }
        uint256 usdtFromReserve = 0;
        if (usdtFromSwap < usdtOut) {
            usdtFromReserve = usdtOut - usdtFromSwap;
            require(usdtFromReserve <= buybackReserve, "exceeds reserve");
        }

        uint256 usdtSurplus = usdtFromSwap > usdtOut ? usdtFromSwap - usdtOut : 0;

        if (pgmUnswapped > 0) { totalPGMBurned += pgmUnswapped; }
        if (usdtFromReserve > 0) { buybackReserve -= usdtFromReserve; }

        if (pgmUnswapped > 0) { IPGMToken(pgm).burn(pgmUnswapped); }

        // LP surplus goes to ecosystem proxy. Wrapped in try/catch so a misconfigured
        // proxy/router can never block redeem(). If forwarding fails, excess is tracked
        // in pendingExcess and retried on the next redeem that generates surplus.
        if (usdtSurplus > 0 || pendingExcess > 0) {
            uint256 toSend = usdtSurplus + pendingExcess;
            try this._forwardExcess(toSend) {
                pendingExcess = 0;
                emit ExcessCollected(toSend);
            } catch {
                pendingExcess = toSend;
            }
        }

        IERC20(USDT).safeTransfer(user, usdtOut);
        if (dust > 0) { IERC20(pgm).safeTransfer(user, dust); }

        emit Redeemed(user, pgmAmount, usdtOut);
    }

    /// @dev External helper for try/catch excess forwarding. Cannot be called by anyone else.
    function _forwardExcess(uint256 amount) external {
        require(msg.sender == address(this), "only self");
        IERC20(USDT).safeTransfer(excessUSDTProxy, amount);
        IExcessUSDTProxy(excessUSDTProxy).deposit(amount);
    }

    /// @dev Redeem-side swap limit. Used in _redeemCore with zeroForOne=false (PGM→USDT).
    ///      For zeroForOne=false the pool requires limit > current sqrtPrice. This value
    ///      is approximately floor / (1 - fee) — i.e. ABOVE the floor sqrt price, so the
    ///      swap can move price UP to slightly past floor (still close enough that
    ///      redemption gets at least the floor amount of USDT).
    uint160 public constant SQRT_PRICE_REDEEM_LIMIT = 2510551445809845255360469847357648927;

    /// @dev Swap limit for smoke test DEX-buy simulation only.
    ///      Used in _lifecycleTest with zeroForOne=true (USDT→PGM). For zeroForOne=true
    ///      the pool requires limit < current sqrtPrice. The Raise contract creates the
    ///      pool at TICK_START_PRICE and mints LP from TICK_MIN..TICK_UPSIDE_START — so
    ///      there is a gap (TICK_UPSIDE_START < tick < TICK_START_PRICE) with no LP. A
    ///      "fee-adjusted floor" limit would land in this gap and the swap would consume
    ///      zero tokens. Using MIN_SQRT_RATIO + 1 (the absolute lower bound on Aborean's
    ///      pool) lets the swap traverse the empty gap, enter the LP, and consume the
    ///      requested input. Smoke test only — production redeem uses a different limit
    ///      and reserve-path fallback.
    uint160 public constant SQRT_PRICE_DEX_BUY_LIMIT = 4295128740;

    // Excess is forwarded directly in redeem() — never accumulates in reserve.

    function collectTradingFees() external nonReentrant {
        _collectTradingFeesCore();
    }
    /// @dev Trading fees from the upside LP are ecosystem earnings.
    ///      Fee collection is claim-neutral: collectible redeemable PGM fees are
    ///      already part of the outstanding claim system before collection.
    ///      This function only changes custody and does not change buybackReserve.
    function _collectTradingFeesCore() internal {
        require(minterRenounced, "not finalized");
        require(priceDiscoveryNFTId != 0, "no NFT");

        uint256 usdtBefore = IERC20(USDT).balanceOf(address(this));
        uint256 pgmBefore = IERC20(pgm).balanceOf(address(this));

        posMgr.collect(INonfungiblePositionManager.CollectParams({
            tokenId: priceDiscoveryNFTId,
            recipient: address(this),
            amount0Max: type(uint128).max,
            amount1Max: type(uint128).max
        }));

        uint256 usdtCollected = IERC20(USDT).balanceOf(address(this)) - usdtBefore;
        uint256 pgmCollected = IERC20(pgm).balanceOf(address(this)) - pgmBefore;

        if (usdtCollected > 0) { IERC20(USDT).safeTransfer(tradingFeeProxy, usdtCollected); }
        if (pgmCollected > 0) { IERC20(pgm).safeTransfer(tradingFeeProxy, pgmCollected); }
        if (usdtCollected > 0 || pgmCollected > 0) {
            ITradingFeeProxy(tradingFeeProxy).deposit(usdtCollected, pgmCollected);
        }

        emit TradingFeesCollected(usdtCollected, pgmCollected);
    }

    function emergencyWithdraw() external nonReentrant {
        require(block.timestamp >= EMERGENCY_DEADLINE, "too early");
        _emergencyWithdrawCore(msg.sender);
    }
    function _emergencyWithdrawCore(address user) internal {
        require(!minterRenounced, "already finalized");
        require(claimed[user] == 0, "already claimed"); // safety: should never happen (claim needs finalized)

        uint256 sacUSDT = sacrificed[user] / PGM_PER_USDT;
        uint256 usdtToReturn = invested[user] > sacUSDT ? invested[user] - sacUSDT : 0;
        require(usdtToReturn > 0, "nothing to withdraw");

        uint256 inv = invested[user];
        uint256 alloc = allocation[user];
        uint256 sac = sacrificed[user];

        invested[user] = 0;
        allocation[user] = 0;
        sacrificed[user] = 0;

        totalRaisedUSDT -= inv;
        totalPGMAllocated -= alloc;
        totalPGMSacrificed -= sac;

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

        IERC20(USDT).safeTransfer(user, usdtToReturn);
        emit EmergencyWithdrawn(user, usdtToReturn);
    }

    /// @notice How many non-LP-backed PGM are still circulating.
    ///
    /// WHY THIS NOT COUNTS IN LP PGM:
    ///
    /// This function intentionally excludes priceDiscoveryPGMAmount (the PGM placed in the
    /// upside LP at finalize). This is NOT a bug — it is the core design of the raise-contract.
    ///
    /// When someone buys PGM out of the upside LP, they pay USDT INTO the LP. That USDT
    /// stays in the LP as principal. If the buyer later redeems, redeem() FIRST sells the
    /// PGM back into the LP (via pool.swap), recovering that USDT. Only the remainder (if any)
    /// comes from buybackReserve.
    ///
    /// Therefore, LP-origin PGM does NOT create a liability on buybackReserve. The USDT to
    /// cover it sits in the LP, not in the reserve. ReserveForNonLPBackedPGMneeded() only needs to track
    /// PGM whose floor exit depends on the reserve.
    ///
    /// This function intentionally tracks only non-LP-backed redeem claims.
    /// LP-origin movements and fee custody effects are handled separately
    /// and are neutral with respect to solvency.
    ///
    /// Trading-fee PGM is excluded from reserve-only accounting here because
    /// this function tracks only non-LP-backed claims whose floor settlement
    /// depends on the reserve (see proof: fee neutrality, L4).
    ///PGMamountThatNeedToBeBurnableByReserve
    /// See also: redeem() sells into LP first, then uses reserve for the remainder.
    function PGMamountThatNeedToBeBurnableByReserve() public view returns (uint256) {
        if (!minterRenounced) return 0;
        uint256 amountPGMneedToBurnInitially = totalPGMMinted - priceDiscoveryPGMAmount;
        return totalPGMBurned >= amountPGMneedToBurnInitially ? 0 : amountPGMneedToBurnInitially - totalPGMBurned;
    }

    /// @notice How much USDT the reserve needs to cover all circulating non-LP-backed PGM at floor price.
    ///         This is reserve-only, ignoring LP-side USDT.
    function ReserveForNonLPBackedPGMneeded() public view returns (uint256) {
        uint256 circulating = PGMamountThatNeedToBeBurnableByReserve();
        return (circulating + PGM_PER_USDT - 1) / PGM_PER_USDT; // ceiling
    }

    function pendingFees() external view returns (uint256 usdtFees, uint256 pgmFees) {
        if (priceDiscoveryNFTId != 0) {
            (,,,,,,,, ,, uint128 owed0, uint128 owed1) = posMgr.positions(priceDiscoveryNFTId);
            usdtFees = uint256(owed0);
            pgmFees = uint256(owed1);
        }
    }

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

    function projectedMaxSupply() external view returns (uint256) {
        return totalPGMAllocated - totalPGMSacrificed + (totalPGMAllocated - totalPGMSacrificed) / 9;
    }

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

    /// @notice Returns true if the reserve alone covers all circulating PGM at floor price.
    ///         This is the conservative solvency check (ignores LP-side USDT).
    function solvencyHoldsReserveOnly() external view returns (bool) {
        return buybackReserve >= ReserveForNonLPBackedPGMneeded();
    }

    /// @notice Returns true if the given PGM amount can be redeemed from reserve alone.
    function redeemableFromReserve(uint256 pgmAmount) external view returns (bool) {
        uint256 usdtOut = pgmAmount / PGM_PER_USDT;
        return usdtOut > 0 && usdtOut <= buybackReserve;
    }

    /// @notice Returns the current lifecycle phase.
    ///         0 = raising, 1 = launched (waiting for finalize), 2 = finalized (live)
    function phase() external view returns (uint8) {
        if (minterRenounced) return 2;
        if (launched) return 1;
        return 0;
    }
}