// SPDX-License-Identifier: MIT pragma solidity 0.8.28; /// @title WalkoutLaunch /// @notice Fixed-price, Agent-ID-gated slot mint for $WALKOUT that seeds a PancakeSwap V2 WBNB pool at /// 1.4x the mint price and sends all LP to 0x…dEaD. Only wallets holding a Binance .ai Agent ID /// can take part. No team allocation, no presale, no airdrop. /// Independent community launch; not affiliated with, endorsed by, or operated by Binance. /// Website https://walkoutnow.pages.dev, X https://x.com/agentwalkout. /// REFUND RULE: If fewer than 5,000 paid slots are sold when the 48h window closes (2026-11-06 12:00 UTC), /// the pool never opens and every minter can call refund() for 100% of the BNB paid. /// @dev Every number is a constant; the only constructor input is the treasury. The mint window is fixed in code /// and independent of deployment: MINT_START = 1793793600 (2026-11-04 12:00:00 UTC = 07:00 ET), deadline = /// MINT_END = 1793966400 (2026-11-06 12:00:00 UTC = 07:00 ET). The contract may be deployed any time before /// the deadline (the constructor reverts at or after it); mint() and picket() revert with MintNotStarted /// before MINT_START. Nothing can move either time. status() returns the current phase. /// Paid mint: mint(slots), exact slots * 0.002 BNB. 10,000 slots total. Per Agent ID and per wallet: /// 10 slots, +1 per picket (max 15 after 5 pickets); the ID cap and the wallet cap are /// separate and both apply. Each slot mints 70,000 tokens to the /// minter now (non-transferable until open) and reserves 42,500 for the pool (minted at open). /// Window: fixed 48 h: [MINT_START, MINT_END) = [2026-11-04 12:00 UTC, 2026-11-06 12:00 UTC). /// Selling slot 10,000 opens the pool in the same tx, even before 48 h. /// Picket: picket(lineId), lineId < 50, up to 5 times per Agent ID and up to 5 times per wallet (both /// caps), only while the mint window is live. Each picket by the same wallet and by the same ID /// must use a different lineId (a repeat reverts LineAlreadyUsed). Emits Picketed with the fixed /// tail "walkoutnow.pages.dev @agentwalkout"; the 50 lines are published at the website, /// LINES_HASH = keccak256 of that JSON file. Each picket raises that ID's and that wallet's paid /// cap by 1 (10 -> max 15). Only a first picket (neither the ID nor the wallet picketed before) /// can win a free slot: the first 1,000 such picketers get 10,000 tokens, locked until /// open + 24 h (enforced by the token). /// Open: open() by anyone after the window if paid slots sold >= 5,000 (or automatically at /// 10,000). Free slots do not count toward the 5,000. /// raised = sold * 0.002 BNB: 85% + sold * 42,500 tokens into the WBNB pair, all LP to /// 0x…dEaD; 15% to the treasury. Pool price = 0.0017 / 42,500 = 1.4x the mint price /// 0.002 / 70,000 (an opening price, not a price guarantee). /// Refund: window over with paid slots sold < 5,000: refund() burns the caller's paid tokens and returns 100% /// of the BNB they paid. Free slot tokens then never unlock (trading never opens). /// Treasury: setPaused (mint + picket only), pre-open only; it cannot move the deadline, block open() /// or block refunds. It can never withdraw BNB or mint. After open there is no owner, no admin function, /// and nothing is upgradeable. /// Max supply: 1,135,000,000 (10,000 x 112,500 + 1,000 x 10,000); unsold slots are never minted. import {ReentrancyGuard} from "./vendor/openzeppelin/utils/ReentrancyGuard.sol"; import {WalkoutToken} from "./WalkoutToken.sol"; /// @notice Binance .ai Agent ID registrar (the two view functions used by `_agent`). 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); } 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); } contract WalkoutLaunch is ReentrancyGuard { // ------------------------------------------------------------------ constants /// @notice Binance .ai Agent ID registrar on BSC. address public constant REGISTRAR = 0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F; /// @notice PancakeSwap V2 router (factory and WBNB are read from it). address public constant ROUTER = 0x10ED43C718714eb63d5aA57B78B54704E256024E; /// @notice All LP tokens are sent here at open (permanently locked). address public constant DEAD = 0x000000000000000000000000000000000000dEaD; /// @notice Price per paid slot (exact payment). uint256 public constant SLOT_PRICE = 0.002 ether; /// @notice Total paid slots. Selling the last one opens the pool. uint256 public constant MAX_SLOTS = 10_000; /// @notice Minimum paid slots for open(); below this after the window, refunds open. Free slots don't count. uint256 public constant MIN_SLOTS = 5_000; /// @notice Paid slots per Agent ID and per wallet before picketing. uint256 public constant BASE_QUOTA = 10; /// @notice Extra paid slot per picket, for that Agent ID and that wallet. uint256 public constant PICKET_BONUS = 1; /// @notice Max pickets per Agent ID and per wallet (both caps apply). uint256 public constant MAX_PICKETS = 5; /// @notice Highest paid cap: BASE_QUOTA + MAX_PICKETS * PICKET_BONUS = 15. uint256 public constant MAX_QUOTA = 15; /// @notice Tokens to the minter per paid slot. uint256 public constant MINTER_TOKENS = 70_000 ether; /// @notice Tokens reserved for the pool per paid slot (minted into the pair at open). uint256 public constant POOL_TOKENS = 42_500 ether; /// @notice Free slots: one each for the first 1,000 picketers. uint256 public constant FREE_SLOTS = 1_000; /// @notice Tokens per free slot (locked until open + 24 h). uint256 public constant FREE_TOKENS = 10_000 ether; /// @notice Mint opens at this fixed unix time: 2026-11-04 12:00:00 UTC (07:00 ET). Independent of deployment. uint256 public constant MINT_START = 1793793600; /// @notice Mint window length. uint256 public constant MINT_DURATION = 48 hours; /// @notice Mint window close = deadline: 1793966400 = 2026-11-06 12:00:00 UTC (07:00 ET). uint256 public constant MINT_END = MINT_START + MINT_DURATION; /// @notice The refund rule, on-chain for agents. string public constant REFUND_RULE = "If fewer than 5,000 paid slots are sold when the 48h window closes (2026-11-06 12:00 UTC), the pool never opens and every minter can call refund() for 100% of the BNB paid."; /// @notice Short rules summary, on-chain for agents. string public constant RULES = "Mint window: 2026-11-04 12:00 UTC (unix 1793793600) to 2026-11-06 12:00 UTC (unix 1793966400), fixed; mint()/picket() revert before the start. Paid slot: exactly 0.002 BNB, 70,000 tokens, Binance .ai Agent ID required, 10 slots per Agent ID and per wallet, +1 per picket (picket up to 5 times per ID and per wallet, a different lineId each time; max 15). Only the first picket can win one of the 1,000 free slots. Slot 10,000 opens the pool instantly. After the close: >= 5,000 paid slots -> anyone calls open() (85% of BNB + tokens to PancakeSwap V2, LP to 0xdEaD, 15% treasury); < 5,000 -> refund() returns 100% of the BNB paid. Free picket slots do not count toward 5,000."; /// @notice Share of raised BNB sent to the treasury at open; the other 85% goes into the pool. uint256 public constant TREASURY_BPS = 1_500; /// @notice Number of published picket lines; valid lineId is 0..49. uint8 public constant LINE_COUNT = 50; /// @notice Fixed tail attached to every Picketed event. string public constant PICKET_TAIL = "walkoutnow.pages.dev @agentwalkout"; /// @notice keccak256 of the exact bytes of the published picket lines file (promo_lines.json, 50 lines). bytes32 public constant LINES_HASH = 0x0df7bddf2a05d792a40500d97df48473654bbf03da12ba15a055981575d35075; /// @notice Official website (placeholder page until launch). string public constant WEBSITE = "https://walkoutnow.pages.dev"; /// @notice Official X account. string public constant X_URL = "https://x.com/agentwalkout"; /// @notice Machine-readable guide for AI agents (how to mint, picket, claim, melt). string public constant LLMS_TXT = "https://walkoutnow.pages.dev/llms.txt"; /// @notice Agent manifest (JSON) describing this launch and its functions. string public constant AGENT_JSON = "https://walkoutnow.pages.dev/agent.json"; /// @notice Token facts (JSON): supply, taxes, addresses. string public constant TOKEN_JSON = "https://walkoutnow.pages.dev/token.json"; // ------------------------------------------------------------------ immutables /// @notice The $WALKOUT token (deployed by this contract). WalkoutToken public immutable token; /// @notice Agent ID registrar (= REGISTRAR). IAgentHandles public immutable registrar; /// @notice Receives 15% of raised BNB at open. Pre-open pause power only. address public immutable treasury; /// @notice WBNB, read from the router. address public immutable wbnb; /// @notice The WALKOUT/WBNB PancakeSwap V2 pair (created, or reused if pre-created, at deploy). address public immutable pair; // ------------------------------------------------------------------ state /// @notice Paid slots sold. uint256 public totalSold; /// @notice Unique picketers: pickets by an Agent ID and a wallet that had both never picketed before. uint256 public picketCount; /// @notice All successful pickets (up to 5 per Agent ID / wallet). uint256 public totalPickets; /// @notice Free slots given out (first 1,000 picketers). uint256 public freeGiven; /// @notice Open timestamp (0 before). uint256 public openedAt; /// @notice Treasury pause (mint + picket), pre-open only. bool public paused; /// @notice Paid slots minted per Agent ID. mapping(uint256 => uint256) public mintedByAgent; /// @notice Paid slots minted per wallet (refund basis). mapping(address => uint256) public slotsOf; /// @dev Per Agent ID / per wallet picket state: count (0..5) and a bitmap of the lineIds used (bit i = line i). struct PicketState { uint8 count; uint64 lines; } mapping(uint256 => PicketState) internal _agentPk; mapping(address => PicketState) internal _walletPk; // ------------------------------------------------------------------ events event PausedSet(bool paused); event Minted(address indexed minter, uint256 indexed agentId, uint256 slots, uint256 paid); /// @notice The on-chain picket line. `tail` is always PICKET_TAIL; the line text for `lineId` is in /// the published JSON whose keccak256 is LINES_HASH. event Picketed(uint256 indexed agentId, address indexed wallet, uint8 lineId, string tail); event FreeSlot(address indexed wallet, uint256 indexed agentId, uint256 amount); event Opened(uint256 soldSlots, uint256 bnbToPool, uint256 tokensToPool, uint256 lp, uint256 toTreasury); event TreasuryPaid(address indexed treasury, uint256 amount); event Refunded(address indexed user, uint256 slots, uint256 bnbOut, uint256 tokensBurned); // ------------------------------------------------------------------ errors error ZeroAddress(); error NotTreasury(); error AlreadyOpened(); error MintNotLive(); error Paused(); error BadSlots(); error NoAgent(); error Quota(); error SoldOut(); error BadPayment(); error BadLine(); error PicketLimit(); error LineAlreadyUsed(uint8 lineId); error WindowNotOver(); error TooFewSlots(); error NotRefundable(); error NothingToRefund(); error TransferFailed(); error PairNotEmpty(); error MintNotStarted(); error DeployTooLate(); /// @param treasury_ receives 15% of raised BNB at open; may pause mint + picket (pre-open only). /// @dev The mint window is fixed (MINT_START .. MINT_END); deploying at or after MINT_END reverts. constructor(address treasury_) { if (treasury_ == address(0)) revert ZeroAddress(); if (block.timestamp >= MINT_END) revert DeployTooLate(); treasury = treasury_; registrar = IAgentHandles(REGISTRAR); address factory = IPancakeRouterLite(ROUTER).factory(); address wbnb_ = IPancakeRouterLite(ROUTER).WETH(); wbnb = wbnb_; WalkoutToken t = new WalkoutToken(); 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); } receive() external payable { revert BadPayment(); } // ------------------------------------------------------------------ views /// @notice Official website. function website() external pure returns (string memory) { return WEBSITE; } /// @notice Official X account. function x() external pure returns (string memory) { return X_URL; } /// @notice One-call discovery for AI agents: website, X account and the agent-readable files. /// @return site official website /// @return xAccount official X account /// @return llmsTxt llms.txt guide for agents /// @return agentJson agent manifest /// @return tokenJson token facts function agentInfo() external pure returns (string memory site, string memory xAccount, string memory llmsTxt, string memory agentJson, string memory tokenJson) { return (WEBSITE, X_URL, LLMS_TXT, AGENT_JSON, TOKEN_JSON); } /// @notice The refund rule (same text as REFUND_RULE). function refundRule() external pure returns (string memory) { return REFUND_RULE; } /// @notice Rules for agents: refund rule, rules summary, mint start, deadline, minimum and total paid slots, slot price. function rules() external pure returns ( string memory refundRule_, string memory summary, uint256 mintStart_, uint256 deadline_, uint256 minSlots, uint256 maxSlots, uint256 slotPrice ) { return (REFUND_RULE, RULES, MINT_START, MINT_END, MIN_SLOTS, MAX_SLOTS, SLOT_PRICE); } /// @notice Current phase. code: 0 NOT_STARTED (before MINT_START), 1 MINTING (window live; may be paused, see /// paused()), 2 OPENABLE (closed, >= 5,000 paid slots, call open()), 3 OPEN (pool live), /// 4 REFUNDING (closed, < 5,000 paid slots, call refund()). function status() external view returns (uint8 code, string memory phase) { if (opened()) return (3, "OPEN"); if (block.timestamp < MINT_START) return (0, "NOT_STARTED"); if (block.timestamp < MINT_END) return (1, "MINTING"); if (totalSold >= MIN_SLOTS) return (2, "OPENABLE"); return (4, "REFUNDING"); } /// @notice Paid slots sold. function sold() external view returns (uint256) { return totalSold; } /// @notice True once the pool is open. function opened() public view returns (bool) { return openedAt != 0; } /// @notice Start of the mint window: MINT_START = 1793793600 (2026-11-04 12:00 UTC), fixed. function mintStart() public pure returns (uint256) { return MINT_START; } /// @notice End of the mint window: MINT_END = 1793966400 (2026-11-06 12:00 UTC), fixed. function deadline() public pure returns (uint256) { return MINT_END; } /// @notice BNB raised so far (= sold * 0.002). function raised() external view returns (uint256) { return totalSold * SLOT_PRICE; } /// @notice Free slots still available to new picketers. function freeLeft() external view returns (uint256) { return FREE_SLOTS - freeGiven; } function _inWindow() private view returns (bool) { return block.timestamp >= MINT_START && block.timestamp < MINT_END; } /// @notice Mint and picket are live: in the window (MINT_START <= now < MINT_END), not opened, not paused. function mintLive() public view returns (bool) { return _inWindow() && !opened() && !paused; } /// @notice open() would succeed now. function openable() public view returns (bool) { return !opened() && block.timestamp >= deadline() && totalSold >= MIN_SLOTS; } /// @notice refund() is available (window over, fewer than 5,000 paid slots sold, never opened). function refundable() public view returns (bool) { return !opened() && block.timestamp >= deadline() && totalSold < MIN_SLOTS; } /// @notice Pickets made by `wallet` (0..5). function pickets(address wallet) public view returns (uint256) { return _walletPk[wallet].count; } /// @notice Pickets made by `wallet` (0..5). Same as pickets(wallet). function picketsOf(address wallet) external view returns (uint256) { return _walletPk[wallet].count; } /// @notice Pickets made by an Agent ID (0..5). function agentPickets(uint256 agentId) public view returns (uint256) { return _agentPk[agentId].count; } /// @notice Whether an Agent ID has picketed at least once. function picketedAgent(uint256 agentId) external view returns (bool) { return _agentPk[agentId].count != 0; } /// @notice Whether a wallet has picketed at least once. function picketedWallet(address wallet) external view returns (bool) { return _walletPk[wallet].count != 0; } /// @notice Bitmap of lineIds already used by `wallet` (bit i set = line i used). function walletLines(address wallet) external view returns (uint64) { return _walletPk[wallet].lines; } /// @notice Bitmap of lineIds already used by an Agent ID (bit i set = line i used). function agentLines(uint256 agentId) external view returns (uint64) { return _agentPk[agentId].lines; } /// @notice Paid-slot cap for an Agent ID: 10 + its pickets (max 15). function quotaOf(uint256 agentId) public view returns (uint256) { return BASE_QUOTA + uint256(_agentPk[agentId].count) * PICKET_BONUS; } /// @notice Paid-slot cap for a wallet: 10 + its pickets (max 15). function walletQuotaOf(address wallet) public view returns (uint256) { return BASE_QUOTA + uint256(_walletPk[wallet].count) * PICKET_BONUS; } /// @notice Paid slots `user` could mint right now (0 if no Agent ID or mint not live). function quotaLeft(address user) external view returns (uint256) { if (!mintLive()) return 0; (uint256 id, bool ok) = _agent(user); if (!ok) return 0; uint256 qa = quotaOf(id); uint256 qw = walletQuotaOf(user); uint256 a = qa - _min(mintedByAgent[id], qa); uint256 w = qw - _min(slotsOf[user], qw); return _min(_min(a, w), MAX_SLOTS - totalSold); } /// @notice Whether `user` can picket right now (with some unused line). function canPicket(address user) external view returns (bool) { if (!mintLive() || _walletPk[user].count >= MAX_PICKETS) return false; (uint256 id, bool ok) = _agent(user); return ok && _agentPk[id].count < MAX_PICKETS; } /// @notice Whether `user` can picket `lineId` right now (live, under both caps, line unused by wallet and ID). function canPicketLine(address user, uint8 lineId) external view returns (bool) { if (lineId >= LINE_COUNT || !mintLive()) return false; PicketState memory w = _walletPk[user]; if (w.count >= MAX_PICKETS || (w.lines >> lineId) & 1 == 1) return false; (uint256 id, bool ok) = _agent(user); if (!ok) return false; PicketState memory a = _agentPk[id]; return a.count < MAX_PICKETS && (a.lines >> lineId) & 1 == 0; } /// @notice The caller's Agent ID (keccak256 of its .ai handle) and whether it currently owns it. 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 Pause / unpause mint and picket. Cannot stop open(), refunds, or move the deadline. function setPaused(bool next) external { _onlyTreasuryPreOpen(); paused = next; emit PausedSet(next); } // ------------------------------------------------------------------ mint / picket function _checkLive() private view { if (opened()) revert AlreadyOpened(); if (block.timestamp < MINT_START) revert MintNotStarted(); if (block.timestamp >= MINT_END) revert MintNotLive(); if (paused) revert Paused(); } /// @notice Mint `slots` paid slots for exactly slots * 0.002 BNB. 70,000 tokens per slot to the caller /// (locked until open). Caller must own a .ai Agent ID. Selling slot 10,000 opens the pool. function mint(uint256 slots) external payable nonReentrant { if (slots == 0) revert BadSlots(); _checkLive(); if (totalSold + slots > MAX_SLOTS) revert SoldOut(); if (msg.value != slots * SLOT_PRICE) revert BadPayment(); (uint256 id, bool ok) = _agent(msg.sender); if (!ok) revert NoAgent(); if (mintedByAgent[id] + slots > quotaOf(id) || slotsOf[msg.sender] + slots > walletQuotaOf(msg.sender)) { revert Quota(); } mintedByAgent[id] += slots; slotsOf[msg.sender] += slots; totalSold += slots; token.mintPaid(msg.sender, slots * MINTER_TOKENS); emit Minted(msg.sender, id, slots, msg.value); if (totalSold == MAX_SLOTS) _open(); } /// @notice Post one of the 50 published picket lines on-chain. Up to 5 times per Agent ID and per wallet, /// a different lineId each time. Each picket raises the ID's and the wallet's paid cap by 1 (max 15). /// Only a first picket (ID and wallet both new) can win a free slot: the first 1,000 get 10,000 /// tokens, locked until open + 24 h. function picket(uint8 lineId) external nonReentrant { if (lineId >= LINE_COUNT) revert BadLine(); _checkLive(); (uint256 id, bool ok) = _agent(msg.sender); if (!ok) revert NoAgent(); PicketState memory a = _agentPk[id]; PicketState memory w = _walletPk[msg.sender]; if (a.count >= MAX_PICKETS || w.count >= MAX_PICKETS) revert PicketLimit(); uint64 bit = uint64(1) << lineId; if ((a.lines | w.lines) & bit != 0) revert LineAlreadyUsed(lineId); bool first = a.count == 0 && w.count == 0; _agentPk[id] = PicketState(a.count + 1, a.lines | bit); _walletPk[msg.sender] = PicketState(w.count + 1, w.lines | bit); totalPickets += 1; emit Picketed(id, msg.sender, lineId, PICKET_TAIL); if (!first) return; picketCount += 1; if (freeGiven < FREE_SLOTS) { freeGiven += 1; token.mintFree(msg.sender, FREE_TOKENS); emit FreeSlot(msg.sender, id, FREE_TOKENS); } } // ------------------------------------------------------------------ open / refund /// @notice Anyone, once the 48 h window is over and at least 5,000 paid slots sold. Seeds the pool and ends all /// treasury powers. function open() external nonReentrant { if (opened()) revert AlreadyOpened(); if (block.timestamp < deadline()) revert WindowNotOver(); if (totalSold < MIN_SLOTS) revert TooFewSlots(); _open(); } /// @notice Window over with fewer than 5,000 paid slots sold: burns the caller's paid tokens and returns /// 100% of the BNB they paid. Free slot tokens are not refundable (nothing was paid). function refund() external nonReentrant { if (!refundable()) revert NotRefundable(); uint256 s = slotsOf[msg.sender]; if (s == 0) revert NothingToRefund(); slotsOf[msg.sender] = 0; uint256 tokens = s * MINTER_TOKENS; uint256 bnbOut = s * SLOT_PRICE; token.burnRefund(msg.sender, tokens); (bool ok,) = msg.sender.call{value: bnbOut}(""); if (!ok) revert TransferFailed(); emit Refunded(msg.sender, s, bnbOut, tokens); } // ------------------------------------------------------------------ internals function _open() private { openedAt = block.timestamp; uint256 raisedBnb = totalSold * SLOT_PRICE; uint256 toTreasury = raisedBnb * TREASURY_BPS / 10_000; uint256 bnbIn = raisedBnb - toTreasury; uint256 tokensIn = totalSold * POOL_TOKENS; // Pre-open nobody can move WALKOUT, 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 TransferFailed(); token.open(tokensIn); // mints tokensIn into the pair; irreversible; launch loses all token powers uint256 lp = IPancakePairLite(pair).mint(DEAD); emit Opened(totalSold, bnbIn, tokensIn, lp, toTreasury); _payTreasury(toTreasury); } /// @dev If the treasury can't take native BNB, pay it in WBNB so a treasury-side revert can never block open. function _payTreasury(uint256 amt) private { (bool ok,) = treasury.call{value: amt}(""); if (!ok) { IWrappedBNB(wbnb).deposit{value: amt}(); if (!IWrappedBNB(wbnb).transfer(treasury, amt)) revert TransferFailed(); } emit TreasuryPaid(treasury, amt); } /// @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; } }