PGM Raise Contract
Raise.sol (includes PGMToken contract)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
// ============================================================================
// Interfaces
// ============================================================================
interface ITreasuryAllocRouter {
function deposit(address user, uint256 pgmAmount, 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 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 IPGMToken {
function mint(address to, uint256 amount) external;
function burn(uint256 amount) external;
function renounceMinter() external;
}
// ============================================================================
// PGM Token — TEST version of PGM
// ============================================================================
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();
}
}
// ============================================================================
// Raise — TEST version of Raise
// All-in-one: deploys own token, creates pool + NFTs in finalize()
// ============================================================================
contract Raise is IERC721Receiver {
using SafeERC20 for IERC20;
// ---- Reentrancy Guard ----
uint256 private _locked = 1;
modifier nonReentrant() {
require(_locked == 1, "reentrant");
_locked = 2;
_;
_locked = 1;
}
// ---- Abstract Chain addresses (hardcoded) ----
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);
// ---- Token (deployed by this contract) ----
address public immutable pgm;
// ---- Pool config (hardcoded from our testing) ----
// USDT is token0, PGM is token1
// sprice tick = 345400 = ~0.001 USDT/PGM
// sqrtPriceX96 at tick 345400 = 2504784100835956094001232597242347520
int24 public constant TICK_SPACING = 200;
int24 public constant TICK_START_PRICE = 345400;
int24 public constant TICK_MAX = 887200; // (887272 / 200) * 200
int24 public constant TICK_MIN = -887200;
uint160 public constant SQRT_PRICE_AT_START = 2504784100835956094001232597242347520;
// USDT pulled from deployer at setup, split into two purposes:
uint256 public constant SETUP_USDT_FOR_TICK_SWAP = 10000; // 0.01 USDT — used in finalize() to move the tick
// When the downside LP is dissolved in refund mode, decreaseLiquidity may return
// 1 USDT-unit less than deposited due to LP math rounding. This buffer ensures
// the last refunder is not short-changed.
uint256 public constant LP_ROUNDING_PROTECTION_USDT = 10000; // 0.01 USDT — goes into buybackReserve
uint256 public constant INITIAL_USDT = SETUP_USDT_FOR_TICK_SWAP + LP_ROUNDING_PROTECTION_USDT;
// ---- Config (set via constructor, identical bytecode for test and production) ----
uint256 public immutable LAUNCH_TIME;
uint256 public immutable EMERGENCY_DEADLINE;
// ---- Pool + NFTs (created in finalize) ----
address public pool;
uint256 public floorProtectionNFTId;
uint256 public priceDiscoveryNFTId;
// ---- State ----
bool public launched;
bool public minterRenounced;
bool public refundMode;
uint256 public buybackReserve;
uint256 public refundReserve;
uint256 public initialUSDTBalance; // USDT from constructor, separate from reserve
uint256 public totalRaisedUSDT;
uint256 public totalPGMAllocated;
uint256 public totalPGMSacrificed;
uint256 public totalPGMMinted;
uint256 public totalPGMBurned;
uint256 public priceDiscoveryPGMAmount;
uint256 public totalFloorProtectionUSDT;
// ---- Per investor ----
mapping(address => uint256) public invested;
mapping(address => uint256) public allocation;
mapping(address => uint256) public sacrificed;
mapping(address => uint256) public claimed;
// ---- Router addresses (immutable, set in constructor, no AddressBook/Truth dependency) ----
address public immutable shopRouter;
address public immutable gameRouter;
address public immutable stakingRouter;
address public immutable badgeRouter;
address public immutable tradingFeeRouter;
address public immutable excessUSDTRouter;
address public immutable referralRegistry;
// ---- Events ----
event Deposited(address indexed investor, uint256 usdt, uint256 pgm);
event Sacrificed(address indexed investor, uint256 pgm);
event Claimed(address indexed investor, uint256 pgm);
event Buyback(address indexed triggerer, uint256 usdtIn, uint256 pgmBurned);
event Launched();
event Finalized(uint256 investorPGM, uint256 priceDiscoveryPGM);
event RefundModeActivated(uint256 usdtInReserve, uint256 pgmBurned);
event Refunded(address indexed user, uint256 pgmIn, uint256 usdtOut);
event Redeemed(address indexed user, uint256 pgmIn, uint256 usdtOut);
// ============================================================================
// CONSTRUCTOR — deploys token, pulls 0.01 USDT from deployer
// ============================================================================
constructor(
uint256 _launchTime,
uint256 _emergencyDeadline,
string memory _tokenName,
string memory _tokenSymbol,
address _shopRouter,
address _gameRouter,
address _stakingRouter,
address _badgeRouter,
address _tradingFeeRouter,
address _excessUSDTRouter,
address _referralRegistry
) {
require(_launchTime > block.timestamp, "launch must be future");
require(_emergencyDeadline > _launchTime, "deadline must be after launch");
require(_shopRouter != address(0), "shopRouter=0");
require(_gameRouter != address(0), "gameRouter=0");
require(_stakingRouter != address(0), "stakingRouter=0");
require(_badgeRouter != address(0), "badgeRouter=0");
require(_tradingFeeRouter != address(0), "tradingFeeRouter=0");
require(_excessUSDTRouter != address(0), "excessUSDTRouter=0");
require(_referralRegistry != address(0), "referralRegistry=0");
LAUNCH_TIME = _launchTime;
EMERGENCY_DEADLINE = _emergencyDeadline;
shopRouter = _shopRouter;
gameRouter = _gameRouter;
stakingRouter = _stakingRouter;
badgeRouter = _badgeRouter;
tradingFeeRouter = _tradingFeeRouter;
excessUSDTRouter = _excessUSDTRouter;
referralRegistry = _referralRegistry;
// Deploy our own token — this contract is the minter
pgm = address(new PGMToken(address(this), _tokenName, _tokenSymbol));
}
/// @notice Send 0.01 USDT to the contract for pool setup.
/// Must be called before finalize(). Anyone can call.
/// Visible on-chain: exactly 0.01 USDT for initial tick positioning.
function fundPoolSetup_USDT_for_tick_positioning() external nonReentrant {
require(initialUSDTBalance == 0, "already funded");
IERC20(USDT).safeTransferFrom(msg.sender, address(this), INITIAL_USDT);
initialUSDTBalance = INITIAL_USDT;
}
// ============================================================================
// ERC721 Receiver (needed to receive NFTs we create)
// ============================================================================
function onERC721Received(address, address, uint256, bytes calldata)
external override returns (bytes4)
{
return IERC721Receiver.onERC721Received.selector;
}
// ============================================================================
// RAISE — deposit USDT
// ============================================================================
/// @notice Deposit USDT without referrer (default referral)
function deposit(uint256 amount) external nonReentrant {
_deposit(msg.sender, amount, address(0));
}
/// @notice Deposit USDT with a referrer wallet
function deposit(uint256 amount, address referrer) external nonReentrant {
_deposit(msg.sender, amount, referrer);
}
function _deposit(address user, uint256 amount, address referrer) internal {
require(!launched, "launched");
require(amount >= 1000, "min 0.001 USDT");
IERC20(USDT).safeTransferFrom(user, address(this), amount);
uint256 pgmAmount = amount * 1e15;
invested[user] += amount;
allocation[user] += pgmAmount;
totalRaisedUSDT += amount;
totalPGMAllocated += pgmAmount;
buybackReserve += amount;
// Register referral on first deposit (no-op on subsequent deposits)
IReferralRegistry(referralRegistry).registerIfNew(user, referrer);
emit Deposited(user, amount, pgmAmount);
}
// ============================================================================
// SACRIFICE — 4 paths, each sends USDT to its TreasuryAllocRouter
// ============================================================================
// --- Sacrifice for self (beneficiary = msg.sender) ---
function sacrificePGMAllocForShop(uint256 pgmAmount) external nonReentrant {
_sacrifice(msg.sender, msg.sender, pgmAmount, shopRouter);
}
function sacrificePGMAllocForGame(uint256 pgmAmount) external nonReentrant {
_sacrifice(msg.sender, msg.sender, pgmAmount, gameRouter);
}
function sacrificePGMAllocForStaking(uint256 pgmAmount) external nonReentrant {
_sacrifice(msg.sender, msg.sender, pgmAmount, stakingRouter);
}
function sacrificePGMAllocForBadge(uint256 pgmAmount) external nonReentrant {
_sacrifice(msg.sender, msg.sender, pgmAmount, badgeRouter);
}
// --- Sacrifice for another wallet (beneficiary receives credit in target system) ---
function sacrificePGMAllocForShop(uint256 pgmAmount, address beneficiary) external nonReentrant {
_sacrifice(msg.sender, beneficiary, pgmAmount, shopRouter);
}
function sacrificePGMAllocForGame(uint256 pgmAmount, address beneficiary) external nonReentrant {
_sacrifice(msg.sender, beneficiary, pgmAmount, gameRouter);
}
function sacrificePGMAllocForStaking(uint256 pgmAmount, address beneficiary) external nonReentrant {
_sacrifice(msg.sender, beneficiary, pgmAmount, stakingRouter);
}
function sacrificePGMAllocForBadge(uint256 pgmAmount, address beneficiary) external nonReentrant {
_sacrifice(msg.sender, beneficiary, pgmAmount, badgeRouter);
}
function _sacrifice(address sacrificer, address beneficiary, uint256 pgmAmount, address target) internal {
require(!launched, "launched");
require(beneficiary != address(0), "beneficiary=0");
require(allocation[sacrificer] - sacrificed[sacrificer] >= pgmAmount, "too much");
sacrificed[sacrificer] += pgmAmount;
totalPGMSacrificed += pgmAmount;
uint256 usdtFreed = pgmAmount / 1e15;
if (usdtFreed > 0) {
require(usdtFreed <= buybackReserve, "not enough reserve");
buybackReserve -= usdtFreed;
// Send USDT and notify the proxy/router (address is immutable, no AddressBook lookup)
// beneficiary is the address that receives credit in the target system
IERC20(USDT).safeTransfer(target, usdtFreed);
ITreasuryAllocRouter(target).deposit(beneficiary, pgmAmount, usdtFreed);
}
emit Sacrificed(sacrificer, pgmAmount);
}
// ============================================================================
// LAUNCH — anyone can trigger after LAUNCH_TIME
// ============================================================================
function launch() external nonReentrant {
require(!launched, "already launched");
require(block.timestamp >= LAUNCH_TIME, "too early");
launched = true;
emit Launched();
}
// ============================================================================
// FINALIZE — creates pool, NFTs, mints tokens, all in one TX
// ============================================================================
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 effectivePGM = totalPGMAllocated - totalPGMSacrificed;
uint256 priceDiscoveryPGM = effectivePGM / 9;
priceDiscoveryPGMAmount = priceDiscoveryPGM;
// Mint all PGM: investor tokens + upside tokens
uint256 totalToMint = effectivePGM + priceDiscoveryPGM;
IPGMToken(pgm).mint(address(this), totalToMint);
totalPGMMinted = totalToMint;
// Determine token order (USDT should be token0 on Abstract)
bool usdtIsToken0 = uint160(USDT) < uint160(pgm);
address token0 = usdtIsToken0 ? USDT : pgm;
address token1 = usdtIsToken0 ? pgm : USDT;
// --- Step 1: Create pool at sprice (tick 345400) ---
pool = clFactory.createPool(token0, token1, TICK_SPACING, SQRT_PRICE_AT_START);
// --- Step 2: Create Upside NFT (100% PGM) ---
// At tick 345400: upside range (-887200 → 345400) has tick = tickUpper → out of range → 100% PGM
IERC20(pgm).approve(POSITION_MANAGER, priceDiscoveryPGM);
{
int24 upsideTickLower = usdtIsToken0 ? TICK_MIN : TICK_START_PRICE;
int24 upsideTickUpper = usdtIsToken0 ? TICK_START_PRICE : TICK_MAX;
uint256 uAmt0 = usdtIsToken0 ? 0 : priceDiscoveryPGM;
uint256 uAmt1 = usdtIsToken0 ? priceDiscoveryPGM : 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(pgm).approve(POSITION_MANAGER, 0);
// --- Step 3: Mini-swap — buy PGM with SETUP_USDT_FOR_TICK_SWAP to move tick below startprice ---
// This pushes the tick below 345400, making downside range out-of-range (100% USDT)
// The remaining SETUP_USDT_DUST_BUFFER goes into buybackReserve to cover LP rounding dust
IERC20(USDT).approve(SWAP_ROUTER, SETUP_USDT_FOR_TICK_SWAP);
uint256 pgmBought = router.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: USDT,
tokenOut: pgm,
tickSpacing: TICK_SPACING,
recipient: address(this),
deadline: block.timestamp + 600,
amountIn: SETUP_USDT_FOR_TICK_SWAP,
amountOutMinimum: 0, // slippage doesn't matter, just moving the tick
sqrtPriceLimitX96: 0
})
);
IERC20(USDT).approve(SWAP_ROUTER, 0);
// Burn the PGM we just bought — it was only to move the tick
IPGMToken(pgm).burn(pgmBought);
// LP rounding protection goes into buybackReserve — ensures last refunder gets full amount
buybackReserve += LP_ROUNDING_PROTECTION_USDT;
initialUSDTBalance = 0;
// --- Step 4: Create Downside NFT + fill with 5% of reserve ---
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;
// --- Step 5: Renounce minter forever ---
IPGMToken(pgm).renounceMinter();
minterRenounced = true;
emit Finalized(effectivePGM, priceDiscoveryPGM);
}
// ============================================================================
// CLAIM — partial claims supported, can send to any address
// ============================================================================
function claim(uint256 amount, address to) external nonReentrant {
require(launched, "not launched");
require(minterRenounced, "not finalized");
require(to != address(0), "zero address");
uint256 maxClaimable = allocation[msg.sender] - sacrificed[msg.sender] - claimed[msg.sender];
require(amount > 0 && amount <= maxClaimable, "bad amount");
claimed[msg.sender] += amount;
IERC20(pgm).safeTransfer(to, amount);
emit Claimed(msg.sender, amount);
}
// ============================================================================
// REDEEM — direct 1:1 at floor price, no swap needed
// Uses buybackReserve first, then refundReserve if in refund mode.
// ============================================================================
// NOTE: redeem makes no sense for the user if token is trading for more than $0.001.
// User must be drunk if he does this.
function redeem(uint256 pgmAmount) external nonReentrant {
require(launched, "not launched");
require(pgmAmount > 0, "zero");
uint256 usdtOut = pgmAmount / 1e15;
require(usdtOut > 0, "too small");
require(IERC20(pgm).balanceOf(msg.sender) >= pgmAmount, "insufficient PGM");
// Auto-activate refund mode if buyback reserve can't cover it
if (usdtOut > buybackReserve && !refundMode && floorProtectionNFTId != 0) {
_activateRefundMode();
}
// Take from buybackReserve first, then refundReserve
uint256 fromBuyback = usdtOut <= buybackReserve ? usdtOut : buybackReserve;
uint256 fromRefund = usdtOut - fromBuyback;
if (fromRefund > 0) {
require(refundMode, "exceeds reserve");
require(fromRefund <= refundReserve, "exceeds all reserves");
}
IERC20(pgm).safeTransferFrom(msg.sender, address(this), pgmAmount);
IPGMToken(pgm).burn(pgmAmount);
totalPGMBurned += pgmAmount;
buybackReserve -= fromBuyback;
refundReserve -= fromRefund;
IERC20(USDT).safeTransfer(msg.sender, usdtOut);
emit Redeemed(msg.sender, pgmAmount, usdtOut);
}
// ============================================================================
// BUYBACK — public, anyone can trigger
// ============================================================================
function triggerBuyback(uint256 usdtAmount) external nonReentrant {
require(launched, "not launched");
require(usdtAmount > 0, "zero amount");
require(usdtAmount <= buybackReserve, "exceeds reserve");
// Auto-activate refund mode if reserve is nearly empty
if (buybackReserve < 1e6 && !refundMode) {
_activateRefundMode();
return;
}
uint256 minPGMOut = usdtAmount * 1e15;
buybackReserve -= usdtAmount;
IERC20(USDT).approve(SWAP_ROUTER, usdtAmount);
uint256 pgmBought = router.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: USDT,
tokenOut: pgm,
tickSpacing: TICK_SPACING,
recipient: address(this),
deadline: block.timestamp + 600,
amountIn: usdtAmount,
amountOutMinimum: minPGMOut,
sqrtPriceLimitX96: 0
})
);
IERC20(USDT).approve(SWAP_ROUTER, 0);
IPGMToken(pgm).burn(pgmBought);
totalPGMBurned += pgmBought;
emit Buyback(msg.sender, usdtAmount, pgmBought);
}
// ============================================================================
// REFUND MODE
// ============================================================================
// Refund mode is activated automatically by redeem() or triggerBuyback()
// when buyback reserve cannot cover the request. No manual trigger needed.
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(pgm);
uint256 usdtCollected;
uint256 pgmCollected;
if (usdtIsToken0) {
usdtCollected = collected0;
pgmCollected = collected1;
} else {
usdtCollected = collected1;
pgmCollected = collected0;
}
if (pgmCollected > 0) {
IPGMToken(pgm).burn(pgmCollected);
totalPGMBurned += pgmCollected;
}
refundReserve = usdtCollected;
totalFloorProtectionUSDT = 0;
refundMode = true;
emit RefundModeActivated(usdtCollected, pgmCollected);
}
// refund() has been merged into redeem() — redeem automatically uses
// refundReserve when buybackReserve is empty and refund mode is active.
// ============================================================================
// EXCESS + FEES
// ============================================================================
function collectExcessUSDT() external nonReentrant {
require(launched, "not launched");
require(_currentTickAboveFloor(), "price below floor");
// excessUSDTRouter is immutable, set in constructor — no AddressBook dependency
uint256 investorMinted = totalPGMMinted - priceDiscoveryPGMAmount;
uint256 investorPGMCirculating = totalPGMBurned >= investorMinted ? 0 : investorMinted - totalPGMBurned;
uint256 usdtNeeded = investorPGMCirculating / 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;
}
IERC20(USDT).safeTransfer(excessUSDTRouter, excess);
}
// SAFETY: Fee collection sends trading fees to tradingFeeRouter (immutable, set in constructor).
// Trading fees are earnings from LP activity — they do not affect the buyback reserve
// or the price floor backing in any way. Public — anyone can call.
function collectTradingFees() external nonReentrant {
// Collect from floor protection NFT
if (floorProtectionNFTId != 0 && !refundMode) {
posMgr.collect(INonfungiblePositionManager.CollectParams({
tokenId: floorProtectionNFTId,
recipient: tradingFeeRouter,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
}));
}
// Collect from price discovery NFT
if (priceDiscoveryNFTId != 0) {
posMgr.collect(INonfungiblePositionManager.CollectParams({
tokenId: priceDiscoveryNFTId,
recipient: tradingFeeRouter,
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();
// USDT is token0: lower tick = higher PGM price
// Price above floor means tick <= TICK_START_PRICE
bool usdtIsToken0 = uint160(USDT) < uint160(pgm);
if (usdtIsToken0) {
return currentTick <= TICK_START_PRICE;
} else {
return currentTick >= TICK_START_PRICE;
}
}
// ============================================================================
// EMERGENCY WITHDRAW
// ============================================================================
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);
totalPGMAllocated -= alloc;
totalPGMSacrificed -= sac;
require(usdtToReturn <= buybackReserve, "insufficient reserve");
buybackReserve -= usdtToReturn;
IERC20(USDT).safeTransfer(msg.sender, usdtToReturn);
}
// ============================================================================
// VIEWS
// ============================================================================
function claimable(address investor) external view returns (uint256) {
if (!launched || !minterRenounced) return 0;
return allocation[investor] - sacrificed[investor] - claimed[investor];
}
function projectedMaxSupply() external view returns (uint256) {
uint256 eff = totalPGMAllocated - totalPGMSacrificed;
return eff + (eff / 9);
}
function stats() external view returns (
uint256 raised, uint256 allocated, uint256 sacr, uint256 reserve, bool live
) {
return (totalRaisedUSDT, totalPGMAllocated, totalPGMSacrificed, buybackReserve, launched);
}
/// @notice Returns all setup and state info in one call — useful for AI auditors and dashboards
function setupInfo() external view returns (
address _pool,
address _pgmToken,
address _usdtToken,
uint256 _floorProtectionNFTId,
uint256 _priceDiscoveryNFTId,
int24 _tickSpacing,
int24 _tickStartPrice,
bool _isLaunched,
bool _isFinalized,
bool _isRefundMode,
uint256 _buybackReserve,
uint256 _refundReserve,
uint256 _totalFloorProtectionUSDT,
uint256 _totalPGMMinted,
uint256 _totalPGMBurned,
uint256 _priceDiscoveryPGMAmount
) {
return (
pool,
pgm,
USDT,
floorProtectionNFTId,
priceDiscoveryNFTId,
TICK_SPACING,
TICK_START_PRICE,
launched,
minterRenounced,
refundMode,
buybackReserve,
refundReserve,
totalFloorProtectionUSDT,
totalPGMMinted,
totalPGMBurned,
priceDiscoveryPGMAmount
);
}
}Last updated
Was this helpful?
