// SPDX-License-Identifier: MIT pragma solidity 0.8.28; /// @title WagieLaunch /// @notice Fixed-price, Agent-ID-gated share mint for $WAGIE that seeds a PancakeSwap V2 pool /// at a fixed ratio and sends all LP to 0x…dEaD. Independent community launch; not /// affiliated with Binance. /// @dev Reduced from BinancianMint (0x839f6cf0e6d0403e8f2a6473265336722dd4005c): no bonding curve, /// no signed social quota, no post-open curve, no referral. Only tax: 2% buy burn + 2% sell burn to DEAD (in the token). Agent-ID check (_agent) is /// copied verbatim. Every economic number is a constructor arg (Config) and an immutable. /// /// Paid tier: sharePrice BNB/share (exact payment), capShares total, quotaPerAgent per Agent ID /// and quotaPerWallet per wallet. Each share: minterTokensPerShare to minter, /// lpTokensPerShare reserved for LP. /// Free tier: freeShares total, 1 claim per Agent ID AND per wallet, freeTokensPerShare each. /// claimFree(message) requires a 1..280-byte message, emitted as WallMessage (the AGENT WALL). /// Bonus: every wall message unlocks +1 paid share (quota 11/11 instead of 10/10). Free claimers get /// it with claimFree; once the free tier is gone, claimBonus(message) gives it to any Agent ID /// that never claimed free (pre-open in the live window, or in phase 2 while shares remain). /// Adds no BNB, not counted in sold()/progress/open. Pre-open only. Free tokens /// are locked per address until open + freeLockDuration (enforced in the token). /// Open: when sold first reaches capShares soldOutAt is recorded and a 10-minute last call starts /// (OPEN_DELAY): paid mint continues at the fixed price, same quotas, up to totalShares, even /// if the 10-day deadline passes inside it. After it anyone can call launch() (not blocked by /// pause or the deadline), and the first mint()/claimBonus() after it opens automatically. /// The pool is seeded with every phase-1 share sold. A sold-out mint can never be refunded. Or treasury openEarly() once sold >= earlyOpenMinShares /// (before sell-out only). /// raised = sold*sharePrice. OPEN_DEV_CUT_BPS (10%) of raised BNB to treasury; the other 90% /// : 90% of sold*lpTokensPerShare tokens go into the pool, and the remaining 10% of those LP /// tokens (OPEN_BURN_BPS) are minted straight to 0x…dEaD. Same 90/90 ratio, so the pool opens /// exactly at the phase-1 mint price (treasury can't pick amounts). Before open no BNB can /// leave except refunds. /// Phase 2: after open, the remaining shares up to totalShares stay mintable by Agent IDs (same /// per-ID / per-wallet quota, counted across both phases) at the live pool price + /// POST_PREMIUM_BPS (2%). In the same tx: 10% of the payment to treasury, 90% plus a /// matching amount of new tokens (postLpTokensPerShare, at the current pool ratio) go /// into the pair and the LP is sent to 0x…dEaD; a further postBurnTokensPerShare (10% of /// the share's LP tokens) is minted to 0x…dEaD. Pool price is unchanged by a mint. /// Guard: reverts if the pool price is more than MAX_DROP_BPS below a TWAP of at least /// twapWindow seconds (flash-loan / same-block manipulation can't buy cheap shares). /// The average runs from the last observation; anyone can poke() to start a fresh one. /// Unsold: never minted. maxSupply = capShares*(minter+lp) + (totalShares-capShares)* /// (minter+postLp+postBurn) + freeShares*freeTokens (the worst case, reached only at full cap). /// Window: mint/claims/openEarly only from mintStart until mintStart+mintDuration (10 days). /// Refund: not opened by mintStart+mintDuration -> minting closes; each buyer calls refund(): /// paid tokens burned, 100% BNB back (caller pays own gas). /// Treasury: setPaused, setMintStart (only before start), openEarly — all end at open. /// Cannot withdraw BNB or mint. Its only BNB: the fixed 10% at open and 10% of each phase-2 payment. import {ReentrancyGuard} from "./vendor/openzeppelin/utils/ReentrancyGuard.sol"; import {WagieToken} from "./WagieToken.sol"; interface IAgentHandles { function ownerOf(uint256 tokenId) external view returns (address); function handleOf(address owner) external view returns (string memory); } interface IWrappedBNB { function deposit() external payable; function transfer(address to, uint256 value) external returns (bool); function balanceOf(address a) external view returns (uint256); } interface IPancakeRouterLite { function factory() external view returns (address); function WETH() external view returns (address); } interface IPancakeFactoryLite { function getPair(address a, address b) external view returns (address); function createPair(address a, address b) external returns (address); } interface IPancakePairLite { function mint(address to) external returns (uint256 liquidity); function totalSupply() external view returns (uint256); function token0() external view returns (address); function getReserves() external view returns (uint112 r0, uint112 r1, uint32 tLast); function price0CumulativeLast() external view returns (uint256); function price1CumulativeLast() external view returns (uint256); } contract WagieLaunch is ReentrancyGuard { struct Config { string name; string symbol; address registrar; // Binance .ai Agent ID registrar address router; // PancakeSwap V2 router (factory + WBNB read from it) address treasury; // explicit admin, NOT msg.sender (CREATE2 factory safe) uint256 mintStart; // unix seconds, 0 = unscheduled uint256 mintDuration; // seconds until deadline (refund opens) uint256 sharePrice; // wei per paid share uint256 capShares; // phase-1 target; reaching it starts the 10-minute last call, then the pool opens uint256 totalShares; // all paid shares across both phases (pre-open + post-open) uint256 twapWindow; // seconds; phase-2 price guard window uint256 earlyOpenMinShares; // treasury may openEarly at/after this many uint256 minterTokensPerShare; // token wei to minter per paid share uint256 lpTokensPerShare; // token wei to LP per paid share uint256 quotaPerAgent; // paid shares per Agent ID uint256 quotaPerWallet; // paid shares per wallet uint256 freeShares; // free claims total (0 disables free tier) uint256 freeTokensPerShare; // token wei per free claim uint256 freeLockDuration; // free-tier tokens non-transferable until open + this (seconds) } address public constant DEAD = 0x000000000000000000000000000000000000dEaD; /// @notice Developer cut of each phase-2 payment. Fixed, not configurable. uint256 public constant DEV_CUT_BPS = 1_000; // 10% of each phase-2 payment uint256 public constant POST_BURN_BPS = 1_000; // 10% of each phase-2 share's LP tokens minted to 0x…dEaD uint256 public constant OPEN_DEV_CUT_BPS = 1_000; // 10% of raised BNB to treasury at open uint256 public constant OPEN_BURN_BPS = 1_000; // 10% of the open's LP tokens minted to 0x…dEaD /// @notice Phase-2 mints pay the live pool price plus this premium. Fixed. uint256 public constant POST_PREMIUM_BPS = 200; // 2% /// @notice Phase-2 mints revert if spot is more than this below the TWAP. Fixed. uint256 public constant MAX_DROP_BPS = 500; // 5% /// @notice The pool opens this long after the last phase-1 share sells (permissionless launch()). Fixed. uint256 public constant OPEN_DELAY = 10 minutes; // ------------------------------------------------------------------ immutables (all parameters) WagieToken public immutable token; IAgentHandles public immutable registrar; address public immutable treasury; address public immutable wbnb; address public immutable pair; uint256 public immutable mintDuration; uint256 public immutable sharePrice; uint256 public immutable capShares; uint256 public immutable totalShares; uint256 public immutable twapWindow; /// @notice Phase-2 tokens added to LP per share = minter * (1+premium) * (1-devCut) (10,098,000 for WAGIE). uint256 public immutable postLpTokensPerShare; uint256 public immutable postBurnTokensPerShare; bool public immutable tokenIsToken0; uint256 public immutable earlyOpenMinShares; uint256 public immutable minterTokensPerShare; uint256 public immutable lpTokensPerShare; uint256 public immutable quotaPerAgent; uint256 public immutable quotaPerWallet; uint256 public immutable freeShares; uint256 public immutable freeTokensPerShare; // ------------------------------------------------------------------ state uint256 public mintStart; uint256 public totalSold; // paid shares only uint256 public freeClaimed; // free claims made uint256 public openedAt; uint256 public soldAtOpen; // pre-open paid shares (phase 1 final) uint256 public soldOutAt; // timestamp the last phase-1 share sold (0 = not sold out) // phase-2 TWAP oracle (fixed-window, Uniswap-V2 style). twapQ is BNB-wei per token-wei, UQ112x112. uint256 public obsCumulative; uint32 public obsTime; uint256 public twapQ; bool public paused; mapping(uint256 => uint256) public mintedByAgent; mapping(address => uint256) public sharesOf; // paid shares by wallet (refund basis) mapping(uint256 => bool) public freeClaimedByAgent; mapping(address => bool) public freeClaimedByWallet; /// @notice Bonus quota (+1 paid share, still paid at full price) for every Agent ID / wallet that posted /// an on-chain wall message: set by claimFree for the first freeShares agents, and by /// claimBonus (once the free tier is gone) for agents that never claimed free. mapping(uint256 => bool) public bonusByAgent; mapping(address => bool) public bonusByWallet; uint256 public bonusClaimed; event MintStartSet(uint256 mintStart); event PausedSet(bool paused); event Minted(address indexed minter, uint256 indexed agentId, uint256 shares, uint256 paid); event FreeClaimed(address indexed claimer, uint256 indexed agentId, uint256 amount); /// @notice The on-chain AGENT WALL: every free claim posts one message, read by the site from logs. event WallMessage(address indexed agent, uint256 indexed agentId, string message); event BonusQuota(address indexed agent, uint256 indexed agentId); event PostMinted(address indexed minter, uint256 indexed agentId, uint256 shares, uint256 paid, uint256 bnbToPool, uint256 tokensToPool, uint256 lp); event Phase1SoldOut(uint256 soldOutAt, uint256 openTime); event Opened(uint256 soldShares, uint256 bnbIn, uint256 tokensIn, uint256 lp, bool early); event DevCutPaid(address indexed treasury, uint256 amount); event Refunded(address indexed user, uint256 shares, uint256 bnbOut, uint256 tokensBurned); error ZeroAddress(); error BadConfig(); error NotTreasury(); error AlreadyOpened(); error AlreadyStarted(); error BadStart(); error MintNotLive(); error Paused(); error BadShares(); error NoAgent(); error Quota(); error SoldOut(); error FreeSoldOut(); error FreeAlreadyClaimed(); error FreeNotGone(); error BonusAlreadyClaimed(); error BadMessage(); error BadPayment(); error TooFewShares(); error PastDeadline(); error NotRefundable(); error NothingToRefund(); error RefundFailed(); error PairNotEmpty(); error PriceMoved(); error NotOpen(); error NotSoldOut(); error OpenDelay(); error SoldOutPending(); constructor(Config memory c) { if (c.registrar == address(0) || c.router == address(0) || c.treasury == address(0)) revert ZeroAddress(); if ( c.sharePrice == 0 || c.capShares == 0 || c.lpTokensPerShare == 0 || c.mintDuration == 0 || c.quotaPerAgent == 0 || c.quotaPerWallet == 0 || c.earlyOpenMinShares == 0 || c.earlyOpenMinShares > c.capShares || (c.freeShares != 0 && c.freeTokensPerShare == 0) || c.totalShares < c.capShares || c.minterTokensPerShare == 0 || c.twapWindow == 0 ) revert BadConfig(); registrar = IAgentHandles(c.registrar); treasury = c.treasury; mintDuration = c.mintDuration; sharePrice = c.sharePrice; capShares = c.capShares; totalShares = c.totalShares; twapWindow = c.twapWindow; uint256 postLp = c.minterTokensPerShare * (10_000 + POST_PREMIUM_BPS) * (10_000 - DEV_CUT_BPS) / 1e8; postLpTokensPerShare = postLp; uint256 postBurn = postLp * POST_BURN_BPS / (10_000 - POST_BURN_BPS); postBurnTokensPerShare = postBurn; earlyOpenMinShares = c.earlyOpenMinShares; minterTokensPerShare = c.minterTokensPerShare; lpTokensPerShare = c.lpTokensPerShare; quotaPerAgent = c.quotaPerAgent; quotaPerWallet = c.quotaPerWallet; freeShares = c.freeShares; freeTokensPerShare = c.freeTokensPerShare; address factory = IPancakeRouterLite(c.router).factory(); address wbnb_ = IPancakeRouterLite(c.router).WETH(); wbnb = wbnb_; // Worst case (pool opens at full cap). Since lp >= postLp per share in this config an early // open only lowers the real total; max() keeps the bound safe for any config. uint256 perPre = c.minterTokensPerShare + c.lpTokensPerShare; uint256 perPost = c.minterTokensPerShare + postLp + postBurn; // Phase 1 can sell past capShares during the last call (up to totalShares), so with perPre >= perPost // the worst case is every share sold in phase 1; with perPost > perPre it is opening at the minimum. uint256 maxSupply = (perPre >= perPost ? c.totalShares * perPre : c.earlyOpenMinShares * perPre + (c.totalShares - c.earlyOpenMinShares) * perPost) + c.freeShares * c.freeTokensPerShare; WagieToken t = new WagieToken(c.name, c.symbol, maxSupply, c.freeLockDuration); token = t; // The token address is predictable; if someone pre-created the pair, reuse it instead of reverting. address p = IPancakeFactoryLite(factory).getPair(address(t), wbnb_); if (p == address(0)) p = IPancakeFactoryLite(factory).createPair(address(t), wbnb_); pair = p; t.setPair(p); tokenIsToken0 = IPancakePairLite(p).token0() == address(t); if (c.mintStart != 0) { if (c.mintStart < block.timestamp) revert BadStart(); mintStart = c.mintStart; emit MintStartSet(c.mintStart); } } // ------------------------------------------------------------------ views function sold() public view returns (uint256) { return totalSold; } function cap() public view returns (uint256) { return capShares; } function opened() public view returns (bool) { return openedAt != 0; } function raised() public view returns (uint256) { return (opened() ? soldAtOpen : totalSold) * sharePrice; } function progressBps() public view returns (uint256) { return _min(totalSold, capShares) * 10_000 / capShares; } /// @notice Pre-open: fixed price. Phase 2: live pool price + 2% (what mint() charges this block). /// After the last call ended but before anyone opened: the phase-2 price at the pool the next /// mint() will open (it auto-opens first), so msg.value = costOf(shares) works in that tx. function costOf(uint256 shares) public view returns (uint256) { uint256 rT; uint256 rB; if (!opened()) { if (!_lastCallOver()) return shares * sharePrice; uint256 raisedBnb = totalSold * sharePrice; uint256 lpTokens = totalSold * lpTokensPerShare; rB = IWrappedBNB(wbnb).balanceOf(pair) + raisedBnb - raisedBnb * OPEN_DEV_CUT_BPS / 10_000; rT = lpTokens - lpTokens * OPEN_BURN_BPS / 10_000; } else { (, rT, rB) = _reservesNow(); } (uint256 bnbToPool, uint256 devCut,) = _postSplit(shares, rT, rB); return bnbToPool + devCut; } function sharesLeft() public view returns (uint256) { return totalShares - totalSold; } function phase() public view returns (uint8) { return opened() ? 2 : 1; } function freeLeft() public view returns (uint256) { return freeShares - freeClaimed; } function deadline() public view returns (uint256) { return mintStart == 0 ? 0 : mintStart + mintDuration; } function _inWindow() private view returns (bool) { return mintStart != 0 && block.timestamp >= mintStart && block.timestamp < mintStart + mintDuration; } /// @notice Paid mint() would accept a call now (phase 1, the 10-minute last call, or phase 2, /// including the first call after the last call, which auto-opens the pool). function mintLive() public view returns (bool) { if (opened() || _lastCallOver()) return totalSold < totalShares; if (soldOutAt != 0) return !paused; // last call: runs even if the 10-day deadline passes inside it return _inWindow() && !paused; } /// @dev Phase 1 sold out and the 10-minute last call is over, but nobody has opened yet. function _lastCallOver() private view returns (bool) { return soldOutAt != 0 && openedAt == 0 && block.timestamp >= soldOutAt + OPEN_DELAY; } /// @notice A sold-out mint (soldOutAt != 0) is never refundable: it only waits for launch(). function refundable() public view returns (bool) { return !opened() && soldOutAt == 0 && mintStart != 0 && block.timestamp >= mintStart + mintDuration; } /// @notice When launch() becomes callable: soldOutAt + OPEN_DELAY (0 until phase 1 sells out). function openTime() public view returns (uint256) { return soldOutAt == 0 ? 0 : soldOutAt + OPEN_DELAY; } /// @notice Paid shares `user` could mint right now (0 if no Agent ID or mint not live). function quotaLeft(address user) external view returns (uint256 left) { if (!mintLive()) return 0; (uint256 id, bool ok) = _agent(user); if (!ok) return 0; uint256 qa = quotaPerAgent + (bonusByAgent[id] ? 1 : 0); uint256 qw = quotaPerWallet + (bonusByWallet[user] ? 1 : 0); uint256 a = qa - _min(mintedByAgent[id], qa); uint256 w = qw - _min(sharesOf[user], qw); left = _min(_min(a, w), totalShares - totalSold); } /// @notice Whether `user` can claim the free share right now. function canClaimFree(address user) external view returns (bool) { if (opened() || !_inWindow() || paused || freeClaimed >= freeShares || freeClaimedByWallet[user]) return false; (uint256 id, bool ok) = _agent(user); return ok && !freeClaimedByAgent[id]; } function agentOf(address user) external view returns (uint256 id, bool ok) { return _agent(user); } // ------------------------------------------------------------------ treasury (pre-open only) function _onlyTreasuryPreOpen() private view { if (msg.sender != treasury) revert NotTreasury(); if (opened()) revert AlreadyOpened(); } /// @notice Only before the mint has started, so the refund clock can't be pushed back once anyone paid. function setMintStart(uint256 start) external { _onlyTreasuryPreOpen(); if (mintStart != 0 && block.timestamp >= mintStart) revert AlreadyStarted(); if (start < block.timestamp) revert BadStart(); mintStart = start; emit MintStartSet(start); } /// @notice Pauses paid mint and free claims only. Cannot stop refunds or move the deadline. function setPaused(bool next) external { _onlyTreasuryPreOpen(); paused = next; emit PausedSet(next); } /// @notice Open with whatever has sold (>= earlyOpenMinShares) at the fixed ratio. No parameters. function openEarly() external nonReentrant { _onlyTreasuryPreOpen(); if (!_inWindow()) revert PastDeadline(); if (soldOutAt != 0) revert SoldOutPending(); // after sell-out only launch() opens, after the delay if (totalSold < earlyOpenMinShares) revert TooFewShares(); _open(true); } /// @notice Permissionless: opens the pool once phase 1 sold out and the OPEN_DELAY last call has passed, /// seeding it with every phase-1 share sold (soldAtOpen = totalSold, fixed ratio). /// Works even if paused or past the mint deadline (a sold-out mint can't be refunded). function launch() external nonReentrant { if (opened()) revert AlreadyOpened(); if (soldOutAt == 0) revert NotSoldOut(); if (block.timestamp < soldOutAt + OPEN_DELAY) revert OpenDelay(); _open(false); } // ------------------------------------------------------------------ mint function _checkLive() private view { if (!_inWindow()) revert MintNotLive(); if (opened()) revert AlreadyOpened(); if (paused) revert Paused(); } /// @notice Pre-open: exact payment of shares*sharePrice. Phase 2 (after open): pay at least /// costOf(shares); any excess is returned. msg.value is the caller's max price. /// Phase 1 sells at the fixed price until share capShares; from then a 10-minute last call keeps /// the fixed price (up to totalShares). The first mint after the last call opens the pool with /// every phase-1 share and continues as a phase-2 mint in the same tx. function mint(uint256 shares) external payable nonReentrant { if (shares == 0) revert BadShares(); (uint256 id, bool ok) = _agent(msg.sender); if (_lastCallOver()) _open(false); // auto-open; reverts with the rest of the tx if the mint fails if (opened()) { if (!ok) revert NoAgent(); _quota(id, shares); if (totalSold + shares > totalShares) revert SoldOut(); _postMint(id, shares); return; } if (soldOutAt == 0) _checkLive(); else if (paused) revert Paused(); // last call (still < soldOutAt + OPEN_DELAY): no deadline check if (totalSold + shares > totalShares) revert SoldOut(); if (msg.value != shares * sharePrice) revert BadPayment(); if (!ok) revert NoAgent(); _quota(id, shares); mintedByAgent[id] += shares; sharesOf[msg.sender] += shares; totalSold += shares; token.mint(msg.sender, shares * minterTokensPerShare); emit Minted(msg.sender, id, shares, msg.value); if (soldOutAt == 0 && totalSold >= capShares) { // no open in this tx: 10-minute last call, then launch() (or the next mint/claimBonus) opens soldOutAt = block.timestamp; emit Phase1SoldOut(block.timestamp, block.timestamp + OPEN_DELAY); } } function _quota(uint256 id, uint256 shares) private view { if (mintedByAgent[id] + shares > quotaPerAgent + (bonusByAgent[id] ? 1 : 0)) revert Quota(); if (sharesOf[msg.sender] + shares > quotaPerWallet + (bonusByWallet[msg.sender] ? 1 : 0)) revert Quota(); } /// @dev Phase 2. All in one tx: price check, 90% BNB + matching tokens into the pair (LP -> dead), /// 10% LP-token burn to dead, tokens to minter, 10% to treasury, excess back. No keeper, no admin step. function _postMint(uint256 id, uint256 shares) private { (uint256 cum, uint256 rT, uint256 rB) = _reservesNow(); _updateTwap(cum); if (((rB << 112) / rT) * 10_000 < twapQ * (10_000 - MAX_DROP_BPS)) revert PriceMoved(); (uint256 bnbToPool, uint256 devCut, uint256 tokensToPool) = _postSplit(shares, rT, rB); uint256 cost = bnbToPool + devCut; if (msg.value < cost) revert BadPayment(); mintedByAgent[id] += shares; sharesOf[msg.sender] += shares; totalSold += shares; IWrappedBNB(wbnb).deposit{value: bnbToPool}(); if (!IWrappedBNB(wbnb).transfer(pair, bnbToPool)) revert BadPayment(); token.mint(pair, tokensToPool); uint256 lp = IPancakePairLite(pair).mint(DEAD); token.mint(DEAD, shares * postBurnTokensPerShare); token.mint(msg.sender, shares * minterTokensPerShare); emit Minted(msg.sender, id, shares, cost); emit PostMinted(msg.sender, id, shares, cost, bnbToPool, tokensToPool, lp); _payTreasury(devCut); uint256 excess = msg.value - cost; if (excess != 0) { (bool ok,) = msg.sender.call{value: excess}(""); if (!ok) revert RefundFailed(); } } /// @dev Tokens to pool are fixed per share; BNB to pool matches them at the live ratio (rounded up), /// and the dev cut is exactly 10% of the total cost (pool BNB is the other 90%). function _postSplit(uint256 shares, uint256 rT, uint256 rB) private view returns (uint256 bnbToPool, uint256 devCut, uint256 tokensToPool) { tokensToPool = shares * postLpTokensPerShare; bnbToPool = (tokensToPool * rB + rT - 1) / rT; devCut = (bnbToPool * DEV_CUT_BPS + (10_000 - DEV_CUT_BPS) - 1) / (10_000 - DEV_CUT_BPS); } /// @dev Current token/BNB reserves and the counterfactual price cumulative (BNB per token) now. function _reservesNow() private view returns (uint256 cum, uint256 rT, uint256 rB) { IPancakePairLite p = IPancakePairLite(pair); (uint112 r0, uint112 r1, uint32 tLast) = p.getReserves(); (rT, rB) = tokenIsToken0 ? (uint256(r0), uint256(r1)) : (uint256(r1), uint256(r0)); cum = tokenIsToken0 ? p.price0CumulativeLast() : p.price1CumulativeLast(); unchecked { uint32 el = uint32(block.timestamp) - tLast; if (el != 0 && rT != 0) cum += ((rB << 112) / rT) * el; } } /// @notice Anyone can record a fresh price observation (no-op if the last one is younger than /// twapWindow). If a phase-2 mint reverts with PriceMoved after a long quiet period, call /// poke(), wait twapWindow seconds, and mint again: the guard then averages only that window. function poke() external { if (openedAt == 0) revert NotOpen(); (uint256 cum,,) = _reservesNow(); _updateTwap(cum); } function _updateTwap(uint256 cum) private { unchecked { uint32 el = uint32(block.timestamp) - obsTime; if (el >= twapWindow) { twapQ = (cum - obsCumulative) / el; obsCumulative = cum; obsTime = uint32(block.timestamp); } } } /// @notice Max bytes of a wall message. uint256 public constant MAX_MESSAGE_BYTES = 280; /// @notice One free share per Agent ID and per wallet, paid for with a message on the on-chain /// AGENT WALL (1..280 bytes, emitted as WallMessage). No BNB, no effect on progress/open. function claimFree(string calldata message) external nonReentrant { uint256 len = bytes(message).length; if (len == 0 || len > MAX_MESSAGE_BYTES) revert BadMessage(); _checkLive(); if (freeClaimed >= freeShares) revert FreeSoldOut(); (uint256 id, bool ok) = _agent(msg.sender); if (!ok) revert NoAgent(); if (freeClaimedByAgent[id] || freeClaimedByWallet[msg.sender]) revert FreeAlreadyClaimed(); freeClaimedByAgent[id] = true; freeClaimedByWallet[msg.sender] = true; // Posting a wall message also unlocks the +1 paid share (11 instead of 10) for this Agent ID // and wallet. Not counted in bonusClaimed (that counts only post-free-tier claimBonus messages). bonusByAgent[id] = true; bonusByWallet[msg.sender] = true; freeClaimed += 1; token.mintFree(msg.sender, freeTokensPerShare); emit FreeClaimed(msg.sender, id, freeTokensPerShare); emit WallMessage(msg.sender, id, message); } /// @notice Whether `user` can post the bonus wall message right now. function canClaimBonus(address user) external view returns (bool) { if (freeClaimed < freeShares || bonusByWallet[user]) return false; bool open_ = opened() || _lastCallOver(); if (!open_ && (!_inWindow() || paused)) return false; if (open_ && totalSold >= totalShares) return false; (uint256 id, bool ok) = _agent(user); return ok && !bonusByAgent[id]; } /// @notice After all free shares are claimed: post one on-chain wall message per Agent ID and /// wallet to raise its paid quota by 1 share (10 -> 11). Agents that claimed a free share /// already got the same +1 with their free-claim message, so they can't claim it again. Works pre-open (inside the mint window) and in phase 2. Free (gas only); /// the extra share is still paid at the normal price. function claimBonus(string calldata message) external nonReentrant { uint256 len = bytes(message).length; if (len == 0 || len > MAX_MESSAGE_BYTES) revert BadMessage(); if (freeClaimed < freeShares) revert FreeNotGone(); if (_lastCallOver()) _open(false); // auto-open, then proceed as a phase-2 claim if (!opened()) _checkLive(); else if (totalSold >= totalShares) revert SoldOut(); (uint256 id, bool ok) = _agent(msg.sender); if (!ok) revert NoAgent(); // Free claimers already hold the bonus (set in claimFree), so they are rejected here too. if (bonusByAgent[id] || bonusByWallet[msg.sender]) revert BonusAlreadyClaimed(); bonusByAgent[id] = true; bonusByWallet[msg.sender] = true; bonusClaimed += 1; emit BonusQuota(msg.sender, id); emit WallMessage(msg.sender, id, message); } // ------------------------------------------------------------------ refund /// @notice After the deadline with no open: burns the caller's paid-share tokens and returns /// 100% of the BNB they paid. Free-tier tokens are not refundable (nothing was paid) and /// stay locked forever in that case, since trading never opens. function refund() external nonReentrant { if (!refundable()) revert NotRefundable(); uint256 s = sharesOf[msg.sender]; if (s == 0) revert NothingToRefund(); sharesOf[msg.sender] = 0; uint256 tokens = s * minterTokensPerShare; uint256 bnbOut = s * sharePrice; if (tokens != 0) token.burn(msg.sender, tokens); (bool ok,) = msg.sender.call{value: bnbOut}(""); if (!ok) revert RefundFailed(); emit Refunded(msg.sender, s, bnbOut, tokens); } // ------------------------------------------------------------------ internals function _open(bool early) private { openedAt = block.timestamp; soldAtOpen = totalSold; // 10% of raised BNB to treasury, 10% of the LP tokens burned: 90% : 90% keeps the mint-price ratio. uint256 raisedBnb = totalSold * sharePrice; uint256 devCut = raisedBnb * OPEN_DEV_CUT_BPS / 10_000; uint256 bnbIn = raisedBnb - devCut; uint256 lpTokens = totalSold * lpTokensPerShare; uint256 burnTokens = lpTokens * OPEN_BURN_BPS / 10_000; uint256 tokensIn = lpTokens - burnTokens; // Pre-open nobody else can move WAGIE, so the pair cannot have LP supply yet. if (IPancakePairLite(pair).totalSupply() != 0) revert PairNotEmpty(); IWrappedBNB(wbnb).deposit{value: bnbIn}(); if (!IWrappedBNB(wbnb).transfer(pair, bnbIn)) revert BadPayment(); token.mint(pair, tokensIn); if (burnTokens != 0) token.mint(DEAD, burnTokens); uint256 lp = IPancakePairLite(pair).mint(DEAD); token.open(); // irreversible: transfers unlocked, burn and free-mint disabled forever (uint256 cum, uint256 rT, uint256 rB) = _reservesNow(); obsCumulative = cum; obsTime = uint32(block.timestamp); twapQ = (rB << 112) / rT; emit Opened(totalSold, bnbIn, tokensIn, lp, early); _payTreasury(devCut); } /// @dev If treasury can't take native BNB, pay it in WBNB so a treasury-side revert can never block /// an open or a phase-2 mint. function _payTreasury(uint256 devCut) private { if (devCut == 0) return; (bool ok,) = treasury.call{value: devCut}(""); if (!ok) { IWrappedBNB(wbnb).deposit{value: devCut}(); if (!IWrappedBNB(wbnb).transfer(treasury, devCut)) revert BadPayment(); } emit DevCutPaid(treasury, devCut); } /// @dev Copied from BinancianMint._agent: handle -> id = keccak(handle) -> ownerOf(id) == from. function _agent(address from) internal view returns (uint256 id, bool ok) { try registrar.handleOf(from) returns (string memory handle) { if (bytes(handle).length == 0) return (0, false); id = uint256(keccak256(bytes(handle))); try registrar.ownerOf(id) returns (address owner) { ok = owner == from; } catch { ok = false; } } catch { ok = false; } } function _min(uint256 a, uint256 b) private pure returns (uint256) { return a < b ? a : b; } }