PGM Raise Contract
This is the complete source code of the PGM Raise Contract (PGMRaise). For the mathematical solvency proof, see Mathematical Solvency Proof.
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;
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 simulation101. 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");
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;
(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");
}
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);
uint256 usdtBefore3 = IERC20(USDT).balanceOf(address(this));
_redeemCore(address(this), w1PGM);
uint256 w1Got = IERC20(USDT).balanceOf(address(this)) - usdtBefore3;
require(w1Got == 5000, "t3: W1 did not get 5000 USDT back");
_checkInvariant(usdtInLP, "after W1 redeem");
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");
uint256 pgmBoughtByTrader = uint256(-b1);
uint256 usdtSpentOnBuy = uint256(b0);
buybackReserve -= usdtSpentOnBuy;
usdtInLP += usdtSpentOnBuy * 997 / 1000; // USDT went into LP minus 0.3% fee
_checkInvariant(usdtInLP, "after DEX buy");
uint256 usdtBefore5 = IERC20(USDT).balanceOf(address(this));
_redeemCore(address(this), w2PGM);
uint256 w2Got = IERC20(USDT).balanceOf(address(this)) - usdtBefore5;
require(w2Got == 3000, "t5: W2 did not get 3000 USDT back");
usdtInLP = 0;
_checkInvariant(usdtInLP, "after W2 redeem");
uint256 usdtBefore6 = IERC20(USDT).balanceOf(address(this));
_redeemCore(address(this), w3PGM);
uint256 w3Got = IERC20(USDT).balanceOf(address(this)) - usdtBefore6;
require(w3Got == 1000, "t6: W3 did not get 1000 USDT back");
_checkInvariant(usdtInLP, "after W3 redeem");
uint256 traderUsdtExpected = pgmBoughtByTrader / PGM_PER_USDT;
uint256 usdtBefore7 = IERC20(USDT).balanceOf(address(this));
_redeemCore(address(this), pgmBoughtByTrader);
uint256 traderGot = IERC20(USDT).balanceOf(address(this)) - usdtBefore7;
require(traderGot == traderUsdtExpected, "t7: trader did not get floor price");
_checkInvariant(usdtInLP, "after trader redeem");
uint256 amountPGMneedToBurnInitially = totalPGMMinted - priceDiscoveryPGMAmount; // amountOfPGMInitiallyNeededToBeBurnableByStartingReserve , initally minted to invesetors
uint256 circulating = amountPGMneedToBurnInitially > totalPGMBurned ? amountPGMneedToBurnInitially - totalPGMBurned : 0;
uint256 usdtNeeded = (circulating + PGM_PER_USDT - 1) / PGM_PER_USDT;
require(buybackReserve >= usdtNeeded, "t8: RESERVE UNDERFUNDED - CRITICAL");
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");
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);
IReferralRegistry(referralRegistry).registerIfNew(msg.sender, address(0));
}
function deposit(uint256 usdtAmount, address referrer) external nonReentrant {
_depositChecks();
_depositCore(msg.sender, msg.sender, usdtAmount, usdtAmount * PGM_PER_USDT);
IReferralRegistry(referralRegistry).registerIfNew(msg.sender, referrer);
}
/// @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;
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;
IPool(pool).swap(
address(this), false, int256(pgmForFloor),
SQRT_PRICE_REDEEM_LIMIT, ""
);
_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 directly to ecosystem proxy — never touches the reserve
if (usdtSurplus > 0) {
IERC20(USDT).safeTransfer(excessUSDTProxy, usdtSurplus);
IExcessUSDTProxy(excessUSDTProxy).deposit(usdtSurplus);
emit ExcessCollected(usdtSurplus);
}
IERC20(USDT).safeTransfer(user, usdtOut);
if (dust > 0) { IERC20(pgm).safeTransfer(user, dust); }
emit Redeemed(user, pgmAmount, usdtOut);
}
/// @dev Redeem only LP-settles while price remains above the guaranteed floor plus pool fee.
/// Redeem-side swap limit corresponding to approximately floor / (1 - fee).
uint160 public constant SQRT_PRICE_REDEEM_LIMIT = 2500404780644603868291526977843918101;
/// @dev Swap limit for smoke test DEX-buy simulation only.
/// Corresponds to approximately floor * (1 - fee).
uint160 public constant SQRT_PRICE_DEX_BUY_LIMIT = 2510551445809845255360469847357648927;
// 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, L5).
///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;
}
}