// SPDX-License-Identifier: MIT pragma solidity 0.8.28; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; interface IAgentIdRegistry { function handleOf(address wallet) external view returns (string memory); function ownerOf(uint256 id) external view returns (address); } interface IV2Factory { function getPair(address tokenA, address tokenB) external view returns (address); function createPair(address tokenA, address tokenB) external returns (address); } interface IV2Pair { function mint(address recipient) external returns (uint256); } interface IWrappedBNB { function deposit() external payable; function balanceOf(address wallet) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); } contract HungerPlatform is ERC20, ReentrancyGuard { enum Phase { Minting, Open, Refund } uint256 public constant LOT_PRICE = 0.002 ether; uint256 public constant TOKENS_PER_LOT = 100_000 ether; uint256 public constant MIN_LOTS = 1_000; uint256 public constant MAX_LOTS = 10_000; uint256 public constant MAX_WALLET_LOTS = 20; uint256 public constant DURATION = 3 hours; uint256 public constant REFUND_DELAY = 24 hours; uint256 public constant BASIS_POINTS = 10_000; uint256 public constant POOL_BPS = 8_500; uint256 public constant TRADE_TAX_BPS = 150; address public constant LP_BURN_ADDRESS = address(0xdEaD); IAgentIdRegistry public immutable registry; IWrappedBNB public immutable wrappedBNB; address payable public immutable projectWallet; address public immutable pair; uint256 public immutable startsAt; uint256 public immutable endsAt; uint256 public immutable refundsAt; Phase public phase; uint256 public lotsMinted; mapping(address => uint256) public purchasedLots; mapping(address => address) public referrerOf; mapping(bytes32 => uint256) private lotsByAgent; mapping(bytes32 => uint256) private referralsByAgent; mapping(bytes32 => bytes32) private referrerByAgent; mapping(address => bool) public refunded; error InvalidConfiguration(); error MintNotStarted(uint256 startsAt); error MintClosed(); error IneligibleWallet(); error InvalidPurchase(uint256 available); error IncorrectPayment(); error SettlementNotReady(); error TransfersLocked(); error NoRefund(); error RefundNotReady(uint256 availableAt); error PaymentFailed(); event ReferralCredited(address indexed buyer, address indexed referrer); event LotsMinted(address indexed buyer, uint256 lots, uint256 paid); event Launch(uint256 lots, uint256 multipleTenths, uint256 poolBNB, uint256 projectBNB, uint256 lpBurned); event RefundsScheduled(uint256 lots, uint256 collected, uint256 availableAt); event Refunded(address indexed buyer, uint256 amount); event ProjectPaidInWrappedBNB(uint256 amount); constructor( IAgentIdRegistry registry_, IV2Factory factory_, IWrappedBNB wrappedBNB_, address payable projectWallet_, uint256 startsAt_ ) ERC20(unicode"饥饿站台", "HUNGER") { if ( address(registry_).code.length == 0 || address(factory_).code.length == 0 || address(wrappedBNB_).code.length == 0 || projectWallet_ == address(0) || projectWallet_ == address(this) || startsAt_ < block.timestamp ) revert InvalidConfiguration(); registry = registry_; wrappedBNB = wrappedBNB_; projectWallet = projectWallet_; address existingPair = factory_.getPair(address(this), address(wrappedBNB_)); pair = existingPair == address(0) ? factory_.createPair(address(this), address(wrappedBNB_)) : existingPair; if (projectWallet_ == pair || pair.code.length == 0) revert InvalidConfiguration(); startsAt = startsAt_; endsAt = startsAt_ + DURATION; refundsAt = startsAt_ + REFUND_DELAY; } function availableLots(address buyer) public view returns (uint256) { if (phase != Phase.Minting || block.timestamp < startsAt || block.timestamp >= endsAt) return 0; bytes32 id = _agentId(buyer); return id == bytes32(0) ? 0 : _availableLots(buyer, id); } function qualifiedReferrals(address wallet) external view returns (uint256) { return referralsByAgent[_agentId(wallet)]; } function _agentId(address wallet) private view returns (bytes32) { if (wallet == address(0)) return bytes32(0); try registry.handleOf(wallet) returns (string memory handle) { if (bytes(handle).length == 0) return bytes32(0); bytes32 id = keccak256(bytes(handle)); try registry.ownerOf(uint256(id)) returns (address owner) { return owner == wallet ? id : bytes32(0); } catch { return bytes32(0); } } catch { return bytes32(0); } } function _availableLots(address buyer, bytes32 id) private view returns (uint256) { (uint256 stageEnd, uint256 baseQuota) = _activeStage(); uint256 allowance = baseQuota + referralsByAgent[id]; if (allowance > MAX_WALLET_LOTS) allowance = MAX_WALLET_LOTS; uint256 bought = lotsByAgent[id]; uint256 walletBought = purchasedLots[buyer]; if (bought >= allowance || walletBought >= MAX_WALLET_LOTS) return 0; uint256 remaining = allowance - bought; uint256 walletRemaining = MAX_WALLET_LOTS - walletBought; if (walletRemaining < remaining) remaining = walletRemaining; uint256 stageRemaining = stageEnd - lotsMinted; return remaining < stageRemaining ? remaining : stageRemaining; } function mint(uint256 lots, address candidateReferrer) external payable nonReentrant { if (block.timestamp < startsAt) revert MintNotStarted(startsAt); if (phase != Phase.Minting || block.timestamp >= endsAt) revert MintClosed(); bytes32 id = _agentId(msg.sender); if (id == bytes32(0)) revert IneligibleWallet(); uint256 available = _availableLots(msg.sender, id); if (lots == 0 || lots > available) revert InvalidPurchase(available); if (msg.value != lots * LOT_PRICE) revert IncorrectPayment(); if (lotsByAgent[id] == 0 && purchasedLots[msg.sender] == 0) { bytes32 referrerId = _agentId(candidateReferrer); if (referrerId != bytes32(0) && referrerId != id && referrerByAgent[referrerId] != id && referrerOf[candidateReferrer] != msg.sender) { referrerByAgent[id] = referrerId; referrerOf[msg.sender] = candidateReferrer; referralsByAgent[referrerId] += 1; emit ReferralCredited(msg.sender, candidateReferrer); } } lotsByAgent[id] += lots; purchasedLots[msg.sender] += lots; lotsMinted += lots; _mint(msg.sender, lots * TOKENS_PER_LOT); emit LotsMinted(msg.sender, lots, msg.value); if (lotsMinted == MAX_LOTS) _settle(); } function settlementReady() public view returns (bool) { return phase == Phase.Minting && block.timestamp >= endsAt; } function settle() external nonReentrant { if (!settlementReady()) revert SettlementNotReady(); _settle(); } function refund() external nonReentrant { uint256 lots = purchasedLots[msg.sender]; if (lotsMinted >= MIN_LOTS || lots == 0 || refunded[msg.sender]) revert NoRefund(); if (block.timestamp < refundsAt) revert RefundNotReady(refundsAt); // A buyer can settle an unprocessed failed round and claim in one transaction. if (settlementReady()) _settle(); if (phase != Phase.Refund) revert NoRefund(); refunded[msg.sender] = true; _burn(msg.sender, lots * TOKENS_PER_LOT); uint256 amount = lots * LOT_PRICE; (bool sent,) = payable(msg.sender).call{value: amount}(""); if (!sent) revert PaymentFailed(); emit Refunded(msg.sender, amount); } function _activeStage() private view returns (uint256 end, uint256 baseQuota) { if (lotsMinted < MIN_LOTS) return (MIN_LOTS, 5); if (lotsMinted < 3_000) return (3_000, 10); if (lotsMinted < 6_000) return (6_000, 15); return (MAX_LOTS, MAX_WALLET_LOTS); } function _openingMultipleTenths() private view returns (uint256 multipleTenths) { multipleTenths = 21; if (lotsMinted >= 3_000) multipleTenths += 25; if (lotsMinted >= 6_000) multipleTenths += 30; if (lotsMinted == MAX_LOTS) multipleTenths += 40; } function _settle() private { uint256 raised = lotsMinted * LOT_PRICE; if (lotsMinted < MIN_LOTS) { phase = Phase.Refund; emit RefundsScheduled(lotsMinted, raised, refundsAt); return; } uint256 poolBNB = raised * POOL_BPS / BASIS_POINTS; uint256 projectBNB = raised - poolBNB; uint256 multipleTenths = _openingMultipleTenths(); // Include any wrapped BNB already donated to the pair when setting its initial price. uint256 startingBNB = poolBNB + wrappedBNB.balanceOf(pair); uint256 poolTokens = startingBNB * TOKENS_PER_LOT * 10 / (LOT_PRICE * multipleTenths); _mint(pair, poolTokens); wrappedBNB.deposit{value: poolBNB}(); if (!wrappedBNB.transfer(pair, poolBNB)) revert PaymentFailed(); uint256 liquidity = IV2Pair(pair).mint(LP_BURN_ADDRESS); phase = Phase.Open; (bool sent,) = projectWallet.call{value: projectBNB, gas: 30_000}(""); if (!sent) { wrappedBNB.deposit{value: projectBNB}(); if (!wrappedBNB.transfer(projectWallet, projectBNB)) revert PaymentFailed(); emit ProjectPaidInWrappedBNB(projectBNB); } emit Launch(lotsMinted, multipleTenths, poolBNB, projectBNB, liquidity); } function _update(address from, address to, uint256 amount) internal override { if (from != address(0) && to != address(0) && amount != 0) { if (phase != Phase.Open) revert TransfersLocked(); if (from == pair || to == pair) { uint256 fee = amount * TRADE_TAX_BPS / BASIS_POINTS; if (fee != 0) super._update(from, projectWallet, fee); amount -= fee; } } super._update(from, to, amount); } }