PGM Raise Contract
PGMRaise (with PGMToken)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
interface IERC721Receiver {
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4);
}
interface INonfungiblePositionManager {
struct MintParams {
address token0;
address token1;
int24 tickSpacing;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
uint160 sqrtPriceX96;
}
function mint(MintParams calldata params)
external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
struct IncreaseLiquidityParams {
uint256 tokenId;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
function increaseLiquidity(IncreaseLiquidityParams calldata params)
external returns (uint128 liquidity, uint256 amount0, uint256 amount1);
struct DecreaseLiquidityParams {
uint256 tokenId;
uint128 liquidity;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
external returns (uint256 amount0, uint256 amount1);
struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}
function collect(CollectParams calldata params)
external returns (uint256 amount0, uint256 amount1);
function positions(uint256 tokenId) external view returns (
uint96 nonce, address operator, address token0, address token1,
int24 tickSpacing, int24 tickLower, int24 tickUpper, uint128 liquidity,
uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0, uint128 tokensOwed1
);
}
interface ISwapRouter {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
int24 tickSpacing;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams calldata params)
external payable returns (uint256 amountOut);
}
interface ICLFactory {
function getPool(address tokenA, address tokenB, int24 tickSpacing) external view returns (address);
function createPool(address tokenA, address tokenB, int24 tickSpacing, uint160 sqrtPriceX96) external returns (address pool);
}
interface IPool {
function slot0() external view returns (
uint160 sqrtPriceX96, int24 tick, uint16 observationIndex,
uint16 observationCardinality, uint16 observationCardinalityNext, bool unlocked
);
}
interface IHermanToken {
function mint(address to, uint256 amount) external;
function burn(uint256 amount) external;
function renounceMinter() external;
}
contract HermanToken is ERC20 {
address public minter;
event MinterRenounced();
constructor(address _minter) ERC20("Herman", "HMAN") {
minter = _minter;
}
function mint(address to, uint256 amount) external {
require(msg.sender == minter, "not minter");
_mint(to, amount);
}
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
function renounceMinter() external {
require(msg.sender == minter, "not minter");
minter = address(0);
emit MinterRenounced();
}
}
contract HermanRaise is IERC721Receiver {
uint256 private _locked = 1;
modifier nonReentrant() {
require(_locked == 1, "reentrant");
_locked = 2;
_;
_locked = 1;
}
address public constant POSITION_MANAGER = 0xa4890B89dC628baE614780079ACc951Fb0ECdC5F;
address public constant SWAP_ROUTER = 0xAda5d0E79681038A9547fe6a59f1413F3E720839;
address public constant CL_FACTORY = 0x8cfE21F272FdFDdf42851f6282c0f998756eEf27;
address public constant USDT = 0x0709F39376dEEe2A2dfC94A58EdEb2Eb9DF012bD;
INonfungiblePositionManager public constant posMgr = INonfungiblePositionManager(POSITION_MANAGER);
ISwapRouter public constant router = ISwapRouter(SWAP_ROUTER);
ICLFactory public constant clFactory = ICLFactory(CL_FACTORY);
address public immutable hman;
int24 public constant TICK_SPACING = 200;
int24 public constant TICK_START_PRICE = 345400;
int24 public constant TICK_MAX = 887200;
int24 public constant TICK_MIN = -887200;
uint160 public constant SQRT_PRICE_AT_START = 2504784100835956094001232597242347520;
uint256 public constant SETUP_USDT_FOR_TICK_SWAP = 10000;
// NOTE: LP rounding protection ensures last refunder gets full amount when downside LP is dissolved
uint256 public constant LP_ROUNDING_PROTECTION_USDT = 10000;
uint256 public constant INITIAL_USDT = SETUP_USDT_FOR_TICK_SWAP + LP_ROUNDING_PROTECTION_USDT;
uint256 public constant LAUNCH_TIME = 1774831405;
uint256 public constant EMERGENCY_DEADLINE = LAUNCH_TIME + 30 days;
address public pool;
uint256 public floorProtectionNFTId;
uint256 public priceDiscoveryNFTId;
bool public launched;
bool public minterRenounced;
bool public refundMode;
uint256 public buybackReserve;
uint256 public refundReserve;
uint256 public initialUSDTBalance;
uint256 public totalRaisedUSDT;
uint256 public totalHMANAllocated;
uint256 public totalHMANSacrificed;
uint256 public totalHMANMinted;
uint256 public totalHMANBurned;
uint256 public priceDiscoveryHMANAmount;
uint256 public totalFloorProtectionUSDT;
mapping(address => uint256) public invested;
mapping(address => uint256) public allocation;
mapping(address => uint256) public sacrificed;
mapping(address => bool) public claimed;
address public admin;
address public gameShopContract;
address public hmanLiquidityContract;
event Deposited(address indexed investor, uint256 usdt, uint256 hman);
event Sacrificed(address indexed investor, uint256 hman);
event Claimed(address indexed investor, uint256 hman);
event Buyback(address indexed triggerer, uint256 usdtIn, uint256 hmanBurned);
event Launched();
event Finalized(uint256 investorHMAN, uint256 priceDiscoveryHMAN);
event RefundModeActivated(uint256 usdtInReserve, uint256 hmanBurned);
event Refunded(address indexed user, uint256 hmanIn, uint256 usdtOut);
event Redeemed(address indexed user, uint256 hmanIn, uint256 usdtOut);
constructor() {
admin = msg.sender;
hman = address(new HermanToken(address(this)));
}
function fundPoolSetup_USDT_for_tick_positioning() external {
require(initialUSDTBalance == 0, "already funded");
require(IERC20(USDT).transferFrom(msg.sender, address(this), INITIAL_USDT), "send USDT");
initialUSDTBalance = INITIAL_USDT;
}
modifier onlyAdmin() {
require(msg.sender == admin, "not admin");
_;
}
function onERC721Received(address, address, uint256, bytes calldata)
external override returns (bytes4)
{
return IERC721Receiver.onERC721Received.selector;
}
function deposit(uint256 amount) external nonReentrant {
require(!launched, "launched");
require(amount >= 1000, "min 0.001 USDT");
require(IERC20(USDT).transferFrom(msg.sender, address(this), amount), "transfer failed");
uint256 hmanAmount = amount * 1e15;
invested[msg.sender] += amount;
allocation[msg.sender] += hmanAmount;
totalRaisedUSDT += amount;
totalHMANAllocated += hmanAmount;
buybackReserve += amount;
emit Deposited(msg.sender, amount, hmanAmount);
}
function sacrificeAllocation(uint256 hmanAmount) external {
require(!launched, "launched");
require(allocation[msg.sender] - sacrificed[msg.sender] >= hmanAmount, "too much");
sacrificed[msg.sender] += hmanAmount;
totalHMANSacrificed += hmanAmount;
uint256 usdtFreed = hmanAmount / 1e15;
if (usdtFreed > 0) {
require(gameShopContract != address(0), "set gameShopContract");
require(usdtFreed <= buybackReserve, "not enough reserve");
buybackReserve -= usdtFreed;
require(IERC20(USDT).transfer(gameShopContract, usdtFreed), "transfer failed");
}
emit Sacrificed(msg.sender, hmanAmount);
}
function launch() external {
require(!launched, "already launched");
require(block.timestamp >= LAUNCH_TIME, "too early");
launched = true;
emit Launched();
}
function finalize() external nonReentrant {
require(launched, "not launched");
require(!minterRenounced, "already finalized");
require(buybackReserve > 0, "nothing raised");
require(initialUSDTBalance > 0, "call fundPoolSetup_USDT_for_tick_positioning first");
uint256 effectiveHMAN = totalHMANAllocated - totalHMANSacrificed;
uint256 priceDiscoveryHMAN = effectiveHMAN / 9;
priceDiscoveryHMANAmount = priceDiscoveryHMAN;
uint256 totalToMint = effectiveHMAN + priceDiscoveryHMAN;
IHermanToken(hman).mint(address(this), totalToMint);
totalHMANMinted = totalToMint;
bool usdtIsToken0 = uint160(USDT) < uint160(hman);
address token0 = usdtIsToken0 ? USDT : hman;
address token1 = usdtIsToken0 ? hman : USDT;
pool = clFactory.createPool(token0, token1, TICK_SPACING, SQRT_PRICE_AT_START);
// NOTE: priceDiscoveryNFT is 100% HMAN, positioned below start price (out of range at creation)
IERC20(hman).approve(POSITION_MANAGER, priceDiscoveryHMAN);
{
int24 upsideTickLower = usdtIsToken0 ? TICK_MIN : TICK_START_PRICE;
int24 upsideTickUpper = usdtIsToken0 ? TICK_START_PRICE : TICK_MAX;
uint256 uAmt0 = usdtIsToken0 ? 0 : priceDiscoveryHMAN;
uint256 uAmt1 = usdtIsToken0 ? priceDiscoveryHMAN : 0;
(uint256 upId,,,) = posMgr.mint(
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
tickSpacing: TICK_SPACING,
tickLower: upsideTickLower,
tickUpper: upsideTickUpper,
amount0Desired: uAmt0,
amount1Desired: uAmt1,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp + 600,
sqrtPriceX96: 0
})
);
priceDiscoveryNFTId = upId;
}
IERC20(hman).approve(POSITION_MANAGER, 0);
// NOTE: Mini-swap moves tick below start price so floorProtectionNFT deposit is 100% USDT
IERC20(USDT).approve(SWAP_ROUTER, SETUP_USDT_FOR_TICK_SWAP);
uint256 hmanBought = router.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: USDT,
tokenOut: hman,
tickSpacing: TICK_SPACING,
recipient: address(this),
deadline: block.timestamp + 600,
amountIn: SETUP_USDT_FOR_TICK_SWAP,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
})
);
IERC20(USDT).approve(SWAP_ROUTER, 0);
IHermanToken(hman).burn(hmanBought);
buybackReserve += LP_ROUNDING_PROTECTION_USDT;
initialUSDTBalance = 0;
// NOTE: floorProtectionNFT gets 5% of reserve as USDT, positioned above start price (out of range)
uint256 toDownside = (buybackReserve * 5) / 100;
buybackReserve -= toDownside;
{
int24 downTickLower = usdtIsToken0 ? TICK_START_PRICE : TICK_MIN;
int24 downTickUpper = usdtIsToken0 ? TICK_MAX : TICK_START_PRICE;
uint256 dAmt0 = usdtIsToken0 ? toDownside : 0;
uint256 dAmt1 = usdtIsToken0 ? 0 : toDownside;
IERC20(USDT).approve(POSITION_MANAGER, toDownside);
(uint256 downId,,,) = posMgr.mint(
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
tickSpacing: TICK_SPACING,
tickLower: downTickLower,
tickUpper: downTickUpper,
amount0Desired: dAmt0,
amount1Desired: dAmt1,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp + 600,
sqrtPriceX96: 0
})
);
floorProtectionNFTId = downId;
IERC20(USDT).approve(POSITION_MANAGER, 0);
}
totalFloorProtectionUSDT = toDownside;
IHermanToken(hman).renounceMinter();
minterRenounced = true;
emit Finalized(effectiveHMAN, priceDiscoveryHMAN);
}
function claim() external nonReentrant {
require(launched, "not launched");
require(minterRenounced, "not finalized");
require(!claimed[msg.sender], "already claimed");
uint256 amount = allocation[msg.sender] - sacrificed[msg.sender];
require(amount > 0, "nothing");
claimed[msg.sender] = true;
require(IERC20(hman).transfer(msg.sender, amount), "transfer failed");
emit Claimed(msg.sender, amount);
}
// NOTE: redeem makes no sense if token trades above $0.001 — direct 1:1 at floor price
function redeem(uint256 hmanAmount) external nonReentrant {
require(launched, "not launched");
require(hmanAmount > 0, "zero");
uint256 usdtOut = hmanAmount / 1e15;
require(usdtOut > 0, "too small");
require(usdtOut <= buybackReserve, "exceeds reserve");
require(IERC20(hman).transferFrom(msg.sender, address(this), hmanAmount), "transfer failed");
IHermanToken(hman).burn(hmanAmount);
totalHMANBurned += hmanAmount;
buybackReserve -= usdtOut;
require(IERC20(USDT).transfer(msg.sender, usdtOut), "transfer failed");
emit Redeemed(msg.sender, hmanAmount, usdtOut);
}
function triggerBuyback(uint256 usdtAmount) external nonReentrant {
require(launched, "not launched");
require(usdtAmount > 0, "zero amount");
require(usdtAmount <= buybackReserve, "exceeds reserve");
if (buybackReserve < 1e6 && !refundMode) {
_activateRefundMode();
return;
}
uint256 minHMANOut = usdtAmount * 1e15;
buybackReserve -= usdtAmount;
IERC20(USDT).approve(SWAP_ROUTER, usdtAmount);
uint256 hmanBought = router.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: USDT,
tokenOut: hman,
tickSpacing: TICK_SPACING,
recipient: address(this),
deadline: block.timestamp + 600,
amountIn: usdtAmount,
amountOutMinimum: minHMANOut,
sqrtPriceLimitX96: 0
})
);
IERC20(USDT).approve(SWAP_ROUTER, 0);
IHermanToken(hman).burn(hmanBought);
totalHMANBurned += hmanBought;
emit Buyback(msg.sender, usdtAmount, hmanBought);
}
function activateRefundMode() external nonReentrant {
require(launched, "not launched");
require(!refundMode, "already active");
require(buybackReserve < 1e6, "reserve not empty");
_activateRefundMode();
}
function _activateRefundMode() internal {
require(floorProtectionNFTId != 0, "no floor protection NFT");
(,,,,,,, uint128 liq,,,,) = posMgr.positions(floorProtectionNFTId);
require(liq > 0, "NFT empty");
posMgr.decreaseLiquidity(INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: floorProtectionNFTId,
liquidity: liq,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp + 600
}));
(uint256 collected0, uint256 collected1) = posMgr.collect(
INonfungiblePositionManager.CollectParams({
tokenId: floorProtectionNFTId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
bool usdtIsToken0 = uint160(USDT) < uint160(hman);
uint256 usdtCollected;
uint256 hmanCollected;
if (usdtIsToken0) {
usdtCollected = collected0;
hmanCollected = collected1;
} else {
usdtCollected = collected1;
hmanCollected = collected0;
}
if (hmanCollected > 0) {
IHermanToken(hman).burn(hmanCollected);
totalHMANBurned += hmanCollected;
}
refundReserve = usdtCollected;
totalFloorProtectionUSDT = 0;
refundMode = true;
emit RefundModeActivated(usdtCollected, hmanCollected);
}
function refund(uint256 hmanAmount) external nonReentrant {
require(refundMode, "refund not active");
require(hmanAmount > 0, "zero amount");
uint256 usdtOut = hmanAmount / 1e15;
require(usdtOut > 0, "amount too small");
require(usdtOut <= refundReserve, "exceeds refund reserve");
require(IERC20(hman).transferFrom(msg.sender, address(this), hmanAmount), "transfer failed");
IHermanToken(hman).burn(hmanAmount);
totalHMANBurned += hmanAmount;
refundReserve -= usdtOut;
require(IERC20(USDT).transfer(msg.sender, usdtOut), "transfer failed");
emit Refunded(msg.sender, hmanAmount, usdtOut);
}
function withdrawExcessUSDT(address to) external nonReentrant onlyAdmin {
require(launched, "not launched");
require(to != address(0), "zero address");
require(_currentTickAboveFloor(), "price below floor");
uint256 investorMinted = totalHMANMinted - priceDiscoveryHMANAmount;
uint256 investorHMANCirculating = totalHMANBurned >= investorMinted ? 0 : investorMinted - totalHMANBurned;
uint256 usdtNeeded = investorHMANCirculating / 1e15;
uint256 usdtFree = buybackReserve + refundReserve;
uint256 totalBacking = usdtFree + totalFloorProtectionUSDT;
require(totalBacking > usdtNeeded, "no excess");
uint256 excess = totalBacking - usdtNeeded;
if (excess > usdtFree) {
excess = usdtFree;
}
require(excess > 0, "no withdrawable excess");
if (excess <= buybackReserve) {
buybackReserve -= excess;
} else {
uint256 remaining = excess - buybackReserve;
buybackReserve = 0;
refundReserve -= remaining;
}
require(IERC20(USDT).transfer(to, excess), "transfer failed");
}
function collectFloorProtectionFees(address feeRecipient) external onlyAdmin {
require(feeRecipient != address(0), "zero address");
require(floorProtectionNFTId != 0 && !refundMode, "no active NFT");
posMgr.collect(INonfungiblePositionManager.CollectParams({
tokenId: floorProtectionNFTId,
recipient: feeRecipient,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
}));
}
function collectPriceDiscoveryFees(address feeRecipient) external onlyAdmin {
require(feeRecipient != address(0), "zero address");
require(priceDiscoveryNFTId != 0, "no price discovery NFT");
posMgr.collect(INonfungiblePositionManager.CollectParams({
tokenId: priceDiscoveryNFTId,
recipient: feeRecipient,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
}));
}
function _currentTickAboveFloor() internal view returns (bool) {
if (refundMode) return true;
if (pool == address(0)) return false;
(, int24 currentTick,,,,) = IPool(pool).slot0();
bool usdtIsToken0 = uint160(USDT) < uint160(hman);
if (usdtIsToken0) {
return currentTick <= TICK_START_PRICE;
} else {
return currentTick >= TICK_START_PRICE;
}
}
function emergencyWithdraw() external nonReentrant {
require(launched, "not launched");
require(!minterRenounced, "already finalized");
require(block.timestamp >= EMERGENCY_DEADLINE, "too early");
uint256 sacUSDT = sacrificed[msg.sender] / 1e15;
uint256 usdtToReturn = invested[msg.sender] > sacUSDT ? invested[msg.sender] - sacUSDT : 0;
require(usdtToReturn > 0, "nothing to withdraw");
uint256 alloc = allocation[msg.sender];
uint256 sac = sacrificed[msg.sender];
invested[msg.sender] = 0;
allocation[msg.sender] = 0;
sacrificed[msg.sender] = 0;
totalRaisedUSDT -= (alloc / 1e15);
totalHMANAllocated -= alloc;
totalHMANSacrificed -= sac;
require(usdtToReturn <= buybackReserve, "insufficient reserve");
buybackReserve -= usdtToReturn;
require(IERC20(USDT).transfer(msg.sender, usdtToReturn), "transfer failed");
}
function setGameShopContract(address _addr) external onlyAdmin {
require(_addr != address(0), "zero address");
gameShopContract = _addr;
}
function setHmanLiquidityContract(address _addr) external onlyAdmin {
require(_addr != address(0), "zero address");
hmanLiquidityContract = _addr;
}
function claimable(address investor) external view returns (uint256) {
if (!launched || !minterRenounced || claimed[investor]) return 0;
return allocation[investor] - sacrificed[investor];
}
function projectedMaxSupply() external view returns (uint256) {
uint256 eff = totalHMANAllocated - totalHMANSacrificed;
return eff + (eff / 9);
}
function stats() external view returns (
uint256 raised, uint256 allocated, uint256 sacr, uint256 reserve, bool live
) {
return (totalRaisedUSDT, totalHMANAllocated, totalHMANSacrificed, buybackReserve, launched);
}
function setupInfo() external view returns (
address _pool,
address _hmanToken,
address _usdtToken,
uint256 _floorProtectionNFTId,
uint256 _priceDiscoveryNFTId,
int24 _tickSpacing,
int24 _tickStartPrice,
address _gameShopContract,
address _hmanLiquidityContract,
address _admin,
bool _isLaunched,
bool _isFinalized,
bool _isRefundMode,
uint256 _buybackReserve,
uint256 _refundReserve,
uint256 _totalFloorProtectionUSDT,
uint256 _totalHMANMinted,
uint256 _totalHMANBurned,
uint256 _priceDiscoveryHMANAmount
) {
return (
pool,
hman,
USDT,
floorProtectionNFTId,
priceDiscoveryNFTId,
TICK_SPACING,
TICK_START_PRICE,
gameShopContract,
hmanLiquidityContract,
admin,
launched,
minterRenounced,
refundMode,
buybackReserve,
refundReserve,
totalFloorProtectionUSDT,
totalHMANMinted,
totalHMANBurned,
priceDiscoveryHMANAmount
);
}
}PGMMultisig
Last updated
Was this helpful?
