{"sources":{"src/RadarFairMint.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.24;\n\ninterface IAgentHandles {\n    function handleOf(address account) external view returns (string memory);\n}\n\ninterface IAgentRadarPass {\n    function passOf(address account) external view returns (uint256);\n    function handleOfPass(uint256 id) external view returns (string memory);\n}\n\ninterface IPancakeFactory {\n    function getPair(address a, address b) external view returns (address);\n    function createPair(address a, address b) external returns (address);\n}\n\ninterface IPancakePair {\n    function mint(address to) external returns (uint256);\n}\n\ninterface IWBNB {\n    function deposit() external payable;\n    function transfer(address to, uint256 value) external returns (bool);\n}\n\n/// @title Agent Radar ($RADAR) fair mint\n/// @notice Wallets holding a Binance Agent ID mint shares at a fixed BNB price. Each share gives a fixed amount of $RADAR,\n///         locked until the pool opens. When the share cap is reached, the contract itself pairs a fixed share of the BNB with\n///         a fixed amount of $RADAR on PancakeSwap V2 and sends the LP tokens to the dead address. If the cap is not reached\n///         before the deadline, every minter can take back exactly what they paid.\n///         There is no owner, no admin function, no keeper, no tax, no pause and no way to mint more $RADAR afterwards. Every\n///         number is set in the constructor and readable on chain.\ncontract RadarFairMint {\n    // ------------------------------------------------------------------ ERC-20\n    string public constant name = \"Agent Radar\";\n    string public constant symbol = \"RADAR\";\n    uint8 public constant decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    event Transfer(address indexed from, address indexed to, uint256 value);\n    event Approval(address indexed owner, address indexed spender, uint256 value);\n\n    // ------------------------------------------------------------------ fair mint (all immutable)\n    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;\n    IAgentHandles public immutable registrar;\n    IAgentRadarPass public immutable pass;\n    address public immutable treasury;\n    IPancakeFactory public immutable factory;\n    address public immutable wbnb;\n    uint256 public immutable sharePrice; // wei per share\n    uint256 public immutable shareCap; // shares that open the pool\n    uint256 public immutable perAgent; // base shares per Agent ID handle\n    uint256 public immutable passBonus; // extra shares when the wallet holds an Agent Radar Pass\n    uint256 public immutable userTokensPerShare; // $RADAR to the minter per share\n    uint256 public immutable poolTokensPerShare; // $RADAR paired in the pool per share\n    uint256 public immutable treasuryBps; // share of the BNB sent to the treasury when the pool opens (rest = liquidity)\n    uint64 public immutable mintStart;\n    uint64 public immutable deadline;\n\n    address public immutable pair; // created at deploy, so the opening mint stays cheap\n\n    uint256 public sharesMinted;\n    bool public opened;\n    uint256 public treasuryOwed; // treasury share kept here if the treasury refused a direct send (anyone can push it later)\n    mapping(address => uint256) public paid; // wei paid by a wallet (refund if the pool never opens)\n    mapping(address => uint256) public sharesOf; // shares minted by a wallet\n    mapping(bytes32 => uint256) public sharesOfHandle; // shares minted per keccak256(handle)\n\n    event Minted(address indexed account, string handle, uint256 shares, uint256 paidWei);\n    event PoolOpened(address pair, uint256 bnbToPool, uint256 tokensToPool, uint256 bnbToTreasury, uint256 lpBurned);\n    event Refunded(address indexed account, uint256 shares, uint256 wei_);\n\n    error BadConfig();\n    error NotStarted();\n    error Ended();\n    error AlreadyOpen();\n    error NoAgentId();\n    error Quota(uint256 left);\n    error SoldOut(uint256 left);\n    error WrongPayment(uint256 expected);\n    error Locked();\n    error NothingToRefund();\n    error RefundClosed();\n    error TransferFailed();\n    error NothingOwed();\n\n    struct Config {\n        address registrar;\n        address pass;\n        address treasury;\n        address factory;\n        address wbnb;\n        uint256 sharePrice;\n        uint256 shareCap;\n        uint256 perAgent;\n        uint256 passBonus;\n        uint256 userTokensPerShare;\n        uint256 poolTokensPerShare;\n        uint256 treasuryBps;\n        uint64 mintStart;\n        uint64 deadline;\n    }\n\n    constructor(Config memory c) {\n        if (\n            c.registrar == address(0) || c.treasury == address(0) || c.factory == address(0) || c.wbnb == address(0)\n                || c.sharePrice == 0 || c.shareCap == 0 || c.perAgent == 0 || c.userTokensPerShare == 0\n                || c.poolTokensPerShare == 0 || c.treasuryBps > 5_000 || c.deadline <= c.mintStart\n            // the pool must open exactly at the mint price: pool tokens per share = user tokens × (1 − treasury share)\n            || c.poolTokensPerShare * 10_000 != c.userTokensPerShare * (10_000 - c.treasuryBps)\n            // PancakeSwap reserves are uint112\n            || c.shareCap * c.poolTokensPerShare >= 2 ** 112 || c.shareCap * c.sharePrice >= 2 ** 112\n        ) revert BadConfig();\n        registrar = IAgentHandles(c.registrar);\n        pass = IAgentRadarPass(c.pass);\n        treasury = c.treasury;\n        factory = IPancakeFactory(c.factory);\n        wbnb = c.wbnb;\n        sharePrice = c.sharePrice;\n        shareCap = c.shareCap;\n        perAgent = c.perAgent;\n        passBonus = c.passBonus;\n        userTokensPerShare = c.userTokensPerShare;\n        poolTokensPerShare = c.poolTokensPerShare;\n        treasuryBps = c.treasuryBps;\n        mintStart = c.mintStart;\n        deadline = c.deadline;\n        address p = IPancakeFactory(c.factory).getPair(address(this), c.wbnb);\n        pair = p == address(0) ? IPancakeFactory(c.factory).createPair(address(this), c.wbnb) : p;\n    }\n\n    // ------------------------------------------------------------------ mint\n\n    /// @notice Shares this wallet may still mint (0 when it has no Agent ID handle).\n    function quotaLeft(address account) public view returns (uint256) {\n        string memory h = registrar.handleOf(account);\n        if (bytes(h).length == 0) return 0;\n        uint256 q = perAgent + (address(pass) != address(0) && _hasPass(account, h) ? passBonus : 0);\n        uint256 used = sharesOfHandle[keccak256(bytes(h))];\n        return used >= q ? 0 : q - used;\n    }\n\n    function mint(uint256 shares) external payable {\n        if (block.timestamp < mintStart) revert NotStarted();\n        if (block.timestamp >= deadline) revert Ended();\n        if (opened) revert AlreadyOpen();\n        string memory h = registrar.handleOf(msg.sender);\n        if (bytes(h).length == 0) revert NoAgentId();\n        uint256 left = quotaLeft(msg.sender);\n        if (shares == 0 || shares > left) revert Quota(left);\n        uint256 capLeft = shareCap - sharesMinted;\n        if (shares > capLeft) revert SoldOut(capLeft);\n        uint256 cost = shares * sharePrice;\n        if (msg.value != cost) revert WrongPayment(cost);\n\n        sharesMinted += shares;\n        sharesOf[msg.sender] += shares;\n        sharesOfHandle[keccak256(bytes(h))] += shares;\n        paid[msg.sender] += cost;\n        _mint(msg.sender, shares * userTokensPerShare);\n        emit Minted(msg.sender, h, shares, cost);\n\n        if (sharesMinted == shareCap) _open();\n    }\n\n    function _open() internal {\n        opened = true;\n        uint256 bnb = address(this).balance;\n        uint256 toTreasury = bnb * treasuryBps / 10_000;\n        uint256 toPool = bnb - toTreasury;\n        uint256 tokens = shareCap * poolTokensPerShare;\n\n        address p = pair;\n        // Direct pair.mint instead of the router: a pair pre-seeded or synced by someone else cannot block or reprice the open\n        // (anything sent to the pair before this point simply stays in the pool).\n        IWBNB(wbnb).deposit{value: toPool}();\n        if (!IWBNB(wbnb).transfer(p, toPool)) revert TransferFailed();\n        _mint(p, tokens);\n        uint256 lp = IPancakePair(p).mint(DEAD);\n\n        // A treasury that refuses BNB cannot block the open: its share simply waits here for claimTreasury().\n        (bool ok,) = treasury.call{value: toTreasury}(\"\");\n        if (!ok) treasuryOwed = toTreasury;\n        emit PoolOpened(p, toPool, tokens, toTreasury, lp);\n    }\n\n    /// @notice Push a treasury share that could not be sent at open. Anyone may call; it only ever pays `treasury`.\n    function claimTreasury() external {\n        uint256 v = treasuryOwed;\n        if (v == 0) revert NothingOwed();\n        treasuryOwed = 0;\n        (bool ok,) = treasury.call{value: v}(\"\");\n        if (!ok) revert TransferFailed();\n    }\n\n    /// @notice If the pool did not open before the deadline, take back exactly what you paid; your locked $RADAR is burned.\n    function refund() external {\n        if (opened) revert RefundClosed();\n        if (block.timestamp < deadline) revert RefundClosed();\n        uint256 w = paid[msg.sender];\n        if (w == 0) revert NothingToRefund();\n        uint256 s = sharesOf[msg.sender];\n        paid[msg.sender] = 0;\n        sharesOf[msg.sender] = 0;\n        _burn(msg.sender, balanceOf[msg.sender]);\n        (bool ok,) = msg.sender.call{value: w}(\"\");\n        if (!ok) revert TransferFailed();\n        emit Refunded(msg.sender, s, w);\n    }\n\n    /// @notice 0 = not started, 1 = minting, 2 = pool open (trading), 3 = deadline passed without opening (refunds).\n    function status() external view returns (uint8) {\n        if (opened) return 2;\n        if (block.timestamp < mintStart) return 0;\n        if (block.timestamp >= deadline) return 3;\n        return 1;\n    }\n\n    // ------------------------------------------------------------------ ERC-20 logic (locked until the pool opens)\n\n    function approve(address spender, uint256 value) external returns (bool) {\n        allowance[msg.sender][spender] = value;\n        emit Approval(msg.sender, spender, value);\n        return true;\n    }\n\n    function transfer(address to, uint256 value) external returns (bool) {\n        _transfer(msg.sender, to, value);\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 value) external returns (bool) {\n        uint256 a = allowance[from][msg.sender];\n        if (a != type(uint256).max) allowance[from][msg.sender] = a - value;\n        _transfer(from, to, value);\n        return true;\n    }\n\n    function _transfer(address from, address to, uint256 value) internal {\n        if (!opened) revert Locked();\n        balanceOf[from] -= value;\n        unchecked {\n            balanceOf[to] += value;\n        }\n        emit Transfer(from, to, value);\n    }\n\n    function _mint(address to, uint256 value) internal {\n        totalSupply += value;\n        unchecked {\n            balanceOf[to] += value;\n        }\n        emit Transfer(address(0), to, value);\n    }\n\n    function _burn(address from, uint256 value) internal {\n        balanceOf[from] -= value;\n        unchecked {\n            totalSupply -= value;\n        }\n        emit Transfer(from, address(0), value);\n    }\n\n    /// @dev The pass must belong to this wallet AND to this same Agent ID handle (one pass = one bonus, per handle).\n    function _hasPass(address account, string memory handle) internal view returns (bool) {\n        try pass.passOf(account) returns (uint256 id) {\n            if (id == 0) return false;\n            try pass.handleOfPass(id) returns (string memory ph) {\n                return keccak256(bytes(ph)) == keccak256(bytes(handle));\n            } catch {\n                return false;\n            }\n        } catch {\n            return false;\n        }\n    }\n\n    receive() external payable {\n        if (msg.sender != wbnb) revert WrongPayment(0);\n    }\n}\n"}},"abi":[{"type":"constructor","inputs":[{"name":"c","type":"tuple","components":[{"name":"registrar","type":"address","internalType":"address"},{"name":"pass","type":"address","internalType":"address"},{"name":"treasury","type":"address","internalType":"address"},{"name":"factory","type":"address","internalType":"address"},{"name":"wbnb","type":"address","internalType":"address"},{"name":"sharePrice","type":"uint256","internalType":"uint256"},{"name":"shareCap","type":"uint256","internalType":"uint256"},{"name":"perAgent","type":"uint256","internalType":"uint256"},{"name":"passBonus","type":"uint256","internalType":"uint256"},{"name":"userTokensPerShare","type":"uint256","internalType":"uint256"},{"name":"poolTokensPerShare","type":"uint256","internalType":"uint256"},{"name":"treasuryBps","type":"uint256","internalType":"uint256"},{"name":"mintStart","type":"uint64","internalType":"uint64"},{"name":"deadline","type":"uint64","internalType":"uint64"}],"internalType":"struct RadarFairMint.Config"}],"stateMutability":"nonpayable"},{"name":"AlreadyOpen","type":"error","inputs":[]},{"name":"BadConfig","type":"error","inputs":[]},{"name":"Ended","type":"error","inputs":[]},{"name":"Locked","type":"error","inputs":[]},{"name":"NoAgentId","type":"error","inputs":[]},{"name":"NotStarted","type":"error","inputs":[]},{"name":"NothingOwed","type":"error","inputs":[]},{"name":"NothingToRefund","type":"error","inputs":[]},{"name":"Quota","type":"error","inputs":[{"name":"left","type":"uint256","internalType":"uint256"}]},{"name":"RefundClosed","type":"error","inputs":[]},{"name":"SoldOut","type":"error","inputs":[{"name":"left","type":"uint256","internalType":"uint256"}]},{"name":"TransferFailed","type":"error","inputs":[]},{"name":"WrongPayment","type":"error","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"}]},{"name":"Approval","type":"event","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"spender","type":"address","indexed":true,"internalType":"address"},{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"Minted","type":"event","inputs":[{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"handle","type":"string","indexed":false,"internalType":"string"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"paidWei","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"PoolOpened","type":"event","inputs":[{"name":"pair","type":"address","indexed":false,"internalType":"address"},{"name":"bnbToPool","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"tokensToPool","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"bnbToTreasury","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"lpBurned","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"Refunded","type":"event","inputs":[{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"wei_","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"Transfer","type":"event","inputs":[{"name":"from","type":"address","indexed":true,"internalType":"address"},{"name":"to","type":"address","indexed":true,"internalType":"address"},{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"DEAD","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"allowance","type":"function","inputs":[{"name":"","type":"address","internalType":"address"},{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"approve","type":"function","inputs":[{"name":"spender","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"name":"balanceOf","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"claimTreasury","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"name":"deadline","type":"function","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"name":"decimals","type":"function","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"name":"factory","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IPancakeFactory"}],"stateMutability":"view"},{"name":"mint","type":"function","inputs":[{"name":"shares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"name":"mintStart","type":"function","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"name":"name","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"opened","type":"function","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"paid","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"pair","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"pass","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IAgentRadarPass"}],"stateMutability":"view"},{"name":"passBonus","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"perAgent","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"poolTokensPerShare","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"quotaLeft","type":"function","inputs":[{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"refund","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"name":"registrar","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IAgentHandles"}],"stateMutability":"view"},{"name":"shareCap","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"sharePrice","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"sharesMinted","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"sharesOf","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"sharesOfHandle","type":"function","inputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"status","type":"function","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"name":"symbol","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"totalSupply","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"transfer","type":"function","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"name":"transferFrom","type":"function","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"name":"treasury","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"treasuryBps","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"treasuryOwed","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"userTokensPerShare","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"wbnb","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"receive","stateMutability":"payable"}],"matchId":"57826581","creationMatch":"exact_match","runtimeMatch":"exact_match","verifiedAt":"2026-10-31T13:08:10Z","match":"exact_match","chainId":"56","address":"0x5015Dc92a0A563bc817Dbe9930900741fDe9Ee9D"}