{"sources":{"contracts/NightBandits.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\n/// @title NightBandits — Night Bandits Club (NBC)\n/// @notice One contract, two jobs: it IS the ERC-721 collection and it runs\n///         the collection's own mint out of the same bytecode (unlike a\n///         separate sale contract bolted onto a token).\n///\n///         The night shift works like this:\n///           * The first `freeTotal` bandits slip in free — one per wallet.\n///           * The remaining `paidTotal` bandits cost `priceWei` apiece —\n///             up to five per wallet.\n///           * Every gatecrasher must show a Binance Agent ID (the gate\n///             registry's balanceOf must be > 0), on BOTH lanes.\n///           * Every mint, free or paid, also pockets the minter exactly\n///             1,000,000 SWAG out of this contract's loot bag, in the\n///             same transaction.\n///           * When both lanes run dry the till settles itself inside the\n///             completing mint; once the window closes anyone may call\n///             settle() instead. Settling sends every wei of BNB and the\n///             LP stash of SWAG to the treasury, and torches whatever loot\n///             was never claimed by sending it to the dead address.\n///           * Nobody owns this contract. Every number that matters is\n///             fixed in the constructor; there is no admin function,\n///             no withdraw, no second chances.\n\ninterface IAgentGate {\n    function balanceOf(address who) external view returns (uint256);\n}\n\ninterface ISwagBag {\n    function balanceOf(address who) external view returns (uint256);\n    function transfer(address dst, uint256 amount) external returns (bool);\n}\n\ninterface IERC721Receiver {\n    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data)\n        external\n        returns (bytes4);\n}\n\ncontract NightBandits {\n    // ---------------- ERC-721 identity ----------------\n    string public constant name = \"Night Bandits Club\";\n    string public constant symbol = \"NBC\";\n\n    // ---------------- Sale constants ----------------\n    uint256 public constant LOOT_PER_MINT = 1_000_000 * 10 ** 18; // SWAG per mint\n    uint256 public constant PAID_WALLET_LIMIT = 5;\n    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;\n\n    // ---------------- Wiring (fixed at birth) ----------------\n    ISwagBag public immutable swag;\n    IAgentGate public immutable gate;\n    address public immutable treasury;\n    string public baseURI;\n\n    uint256 public immutable priceWei;\n    uint256 public immutable freeTotal;\n    uint256 public immutable paidTotal;\n    uint256 public immutable supplyCap; // freeTotal + paidTotal\n    uint256 public immutable lpStash; // SWAG reserved for the LP, paid out at settle\n    uint256 public immutable opensAt;\n    uint256 public immutable closesAt;\n\n    // ---------------- Counters ----------------\n    uint256 public mintedTotal;\n    uint256 public freeUsed;\n    uint256 public paidUsed;\n    bool public settled;\n\n    mapping(address => bool) public freeTakenBy;\n    mapping(address => uint256) public paidTakenBy;\n\n    // ---------------- ERC-721 bookkeeping ----------------\n    mapping(uint256 => address) private _keeper; // tokenId -> current holder\n    mapping(address => uint256) private _kept; // holder -> how many bandits\n    mapping(uint256 => address) private _okayed; // tokenId -> approved spender\n    mapping(address => mapping(address => bool)) private _crew; // owner -> operator -> yes/no\n\n    // ---------------- Events ----------------\n    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\n    event Approval(address indexed holder, address indexed spender, uint256 indexed tokenId);\n    event ApprovalForAll(address indexed holder, address indexed operator, bool approved);\n    event BanditMinted(address indexed member, uint256 indexed tokenId, bool viaFreeLane);\n    event LootPaid(address indexed member, uint256 amount);\n    event SaleSettled(address indexed treasury, uint256 bnbAmount, uint256 lpAmount, uint256 torchedAmount);\n\n    // ---------------- Errors ----------------\n    error NoAgentId();\n    error NightOver(); // settled, or past the window\n    error FreeLaneDrained();\n    error PaidLaneDrained();\n    error FreeAlreadyYours();\n    error PaidLimitHit();\n    error CountMustBePositive();\n    error WrongCoin(uint256 expected, uint256 sent);\n    error LootTransferFailed();\n    error BnbTransferFailed();\n    error SettleTooEarly();\n    error AlreadySettled();\n    error NoSuchBandit(uint256 tokenId);\n    error NotYourBandit();\n    error NotAllowed();\n    error ZeroAddress();\n    error LootBagShort();\n\n    bool private _busy;\n    modifier guard() {\n        require(!_busy, \"NBC: busy\");\n        _busy = true;\n        _;\n        _busy = false;\n    }\n\n    constructor(\n        address swag_,\n        address gate_,\n        address treasury_,\n        string memory baseURI_,\n        uint256 priceWei_,\n        uint256 freeTotal_,\n        uint256 paidTotal_,\n        uint256 lpReserve_,\n        uint256 saleDuration_\n    ) {\n        if (swag_ == address(0) || gate_ == address(0) || treasury_ == address(0)) {\n            revert ZeroAddress();\n        }\n        swag = ISwagBag(swag_);\n        gate = IAgentGate(gate_);\n        treasury = treasury_;\n        baseURI = baseURI_;\n        priceWei = priceWei_;\n        freeTotal = freeTotal_;\n        paidTotal = paidTotal_;\n        supplyCap = freeTotal_ + paidTotal_;\n        lpStash = lpReserve_;\n        opensAt = block.timestamp;\n        closesAt = block.timestamp + saleDuration_;\n    }\n\n    // ================= Sale =================\n\n    /// @notice Sneak one bandit out for free. One per wallet, Agent ID\n    ///         required, and the 1,000,000 SWAG loot comes with it.\n    function mintFree() external guard {\n        _requireOpen();\n        _requireGate(msg.sender);\n        if (freeTakenBy[msg.sender]) revert FreeAlreadyYours();\n        if (freeUsed >= freeTotal) revert FreeLaneDrained();\n\n        freeTakenBy[msg.sender] = true;\n        freeUsed += 1;\n        uint256 id = _strike(msg.sender);\n        emit BanditMinted(msg.sender, id, true);\n        _payLoot(msg.sender, LOOT_PER_MINT);\n\n        _settleIfDrained();\n    }\n\n    /// @notice Buy `count` bandits at priceWei apiece (exact change only),\n    ///         up to the per-wallet limit of five paid. Each one carries\n    ///         its 1,000,000 SWAG loot.\n    function mintPaid(uint256 count) external payable guard {\n        _requireOpen();\n        _requireGate(msg.sender);\n        if (count == 0) revert CountMustBePositive();\n        if (paidTakenBy[msg.sender] + count > PAID_WALLET_LIMIT) revert PaidLimitHit();\n        if (paidUsed + count > paidTotal) revert PaidLaneDrained();\n        uint256 due = count * priceWei;\n        if (msg.value != due) revert WrongCoin(due, msg.value);\n\n        paidTakenBy[msg.sender] += count;\n        paidUsed += count;\n        for (uint256 i = 0; i < count; i++) {\n            uint256 id = _strike(msg.sender);\n            emit BanditMinted(msg.sender, id, false);\n        }\n        _payLoot(msg.sender, count * LOOT_PER_MINT);\n\n        _settleIfDrained();\n    }\n\n    /// @notice Close the night out. Anyone may call once both lanes are\n    ///         drained or the window has shut. BNB and the LP stash go to\n    ///         the treasury; unclaimed loot is torched to the dead address.\n    function settle() external guard {\n        if (settled) revert AlreadySettled();\n        bool drained = freeUsed >= freeTotal && paidUsed >= paidTotal;\n        if (!drained && block.timestamp < closesAt) revert SettleTooEarly();\n        _settle();\n    }\n\n    function _settleIfDrained() internal {\n        if (!settled && freeUsed >= freeTotal && paidUsed >= paidTotal) {\n            _settle();\n        }\n    }\n\n    function _settle() internal {\n        settled = true;\n\n        uint256 lp = lpStash;\n        if (swag.balanceOf(address(this)) < lp) revert LootBagShort();\n\n        uint256 bnb = address(this).balance;\n        if (bnb > 0) {\n            (bool ok, ) = treasury.call{value: bnb}(\"\");\n            if (!ok) revert BnbTransferFailed();\n        }\n        if (lp > 0 && !swag.transfer(treasury, lp)) revert LootTransferFailed();\n\n        uint256 leftover = swag.balanceOf(address(this));\n        if (leftover > 0 && !swag.transfer(DEAD, leftover)) revert LootTransferFailed();\n\n        emit SaleSettled(treasury, bnb, lp, leftover);\n    }\n\n    function _requireOpen() internal view {\n        if (settled || block.timestamp > closesAt) revert NightOver();\n    }\n\n    function _requireGate(address who) internal view {\n        if (gate.balanceOf(who) == 0) revert NoAgentId();\n    }\n\n    function _payLoot(address to, uint256 amount) internal {\n        if (!swag.transfer(to, amount)) revert LootTransferFailed();\n        emit LootPaid(to, amount);\n    }\n\n    // ================= ERC-721 core =================\n\n    function balanceOf(address holder) external view returns (uint256) {\n        if (holder == address(0)) revert ZeroAddress();\n        return _kept[holder];\n    }\n\n    function ownerOf(uint256 tokenId) public view returns (address) {\n        address holder = _keeper[tokenId];\n        if (holder == address(0)) revert NoSuchBandit(tokenId);\n        return holder;\n    }\n\n    function tokenURI(uint256 tokenId) external view returns (string memory) {\n        if (_keeper[tokenId] == address(0)) revert NoSuchBandit(tokenId);\n        return string.concat(baseURI, _digits(tokenId), \".json\");\n    }\n\n    function contractURI() external view returns (string memory) {\n        return string.concat(baseURI, \"collection.json\");\n    }\n\n    function approve(address spender, uint256 tokenId) external {\n        address holder = ownerOf(tokenId);\n        if (msg.sender != holder && !_crew[holder][msg.sender]) revert NotAllowed();\n        _okayed[tokenId] = spender;\n        emit Approval(holder, spender, tokenId);\n    }\n\n    function getApproved(uint256 tokenId) external view returns (address) {\n        if (_keeper[tokenId] == address(0)) revert NoSuchBandit(tokenId);\n        return _okayed[tokenId];\n    }\n\n    function setApprovalForAll(address operator, bool approved) external {\n        _crew[msg.sender][operator] = approved;\n        emit ApprovalForAll(msg.sender, operator, approved);\n    }\n\n    function isApprovedForAll(address holder, address operator) external view returns (bool) {\n        return _crew[holder][operator];\n    }\n\n    function transferFrom(address from, address to, uint256 tokenId) public {\n        address holder = ownerOf(tokenId);\n        if (holder != from) revert NotYourBandit();\n        if (to == address(0)) revert ZeroAddress();\n        if (msg.sender != holder && msg.sender != _okayed[tokenId] && !_crew[holder][msg.sender]) {\n            revert NotAllowed();\n        }\n        delete _okayed[tokenId];\n        unchecked {\n            _kept[from] -= 1;\n            _kept[to] += 1;\n        }\n        _keeper[tokenId] = to;\n        emit Transfer(from, to, tokenId);\n    }\n\n    function safeTransferFrom(address from, address to, uint256 tokenId) external {\n        safeTransferFrom(from, to, tokenId, \"\");\n    }\n\n    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public {\n        transferFrom(from, to, tokenId);\n        if (to.code.length > 0) {\n            bytes4 magic = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data);\n            require(magic == IERC721Receiver.onERC721Received.selector, \"NBC: receiver refused\");\n        }\n    }\n\n    function supportsInterface(bytes4 interfaceId) external pure returns (bool) {\n        return interfaceId == 0x01ffc9a7 // ERC-165\n            || interfaceId == 0x80ac58cd // ERC-721\n            || interfaceId == 0x5b5e139f; // ERC-721 metadata\n    }\n\n    /// @notice Hand the next token id to `to`. Sale counters are the\n    ///         caller's business; this only does the bookkeeping.\n    function _strike(address to) internal returns (uint256 id) {\n        id = ++mintedTotal;\n        _keeper[id] = to;\n        _kept[to] += 1;\n        emit Transfer(address(0), to, id);\n    }\n\n    function _digits(uint256 n) internal pure returns (string memory) {\n        if (n == 0) return \"0\";\n        uint256 len;\n        for (uint256 t = n; t > 0; t /= 10) len++;\n        bytes memory out = new bytes(len);\n        for (uint256 t = n; t > 0; t /= 10) {\n            out[--len] = bytes1(uint8(48 + (t % 10)));\n        }\n        return string(out);\n    }\n}\n"}},"abi":[{"type":"constructor","inputs":[{"name":"swag_","type":"address","internalType":"address"},{"name":"gate_","type":"address","internalType":"address"},{"name":"treasury_","type":"address","internalType":"address"},{"name":"baseURI_","type":"string","internalType":"string"},{"name":"priceWei_","type":"uint256","internalType":"uint256"},{"name":"freeTotal_","type":"uint256","internalType":"uint256"},{"name":"paidTotal_","type":"uint256","internalType":"uint256"},{"name":"lpReserve_","type":"uint256","internalType":"uint256"},{"name":"saleDuration_","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"name":"AlreadySettled","type":"error","inputs":[]},{"name":"BnbTransferFailed","type":"error","inputs":[]},{"name":"CountMustBePositive","type":"error","inputs":[]},{"name":"FreeAlreadyYours","type":"error","inputs":[]},{"name":"FreeLaneDrained","type":"error","inputs":[]},{"name":"LootBagShort","type":"error","inputs":[]},{"name":"LootTransferFailed","type":"error","inputs":[]},{"name":"NightOver","type":"error","inputs":[]},{"name":"NoAgentId","type":"error","inputs":[]},{"name":"NoSuchBandit","type":"error","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}]},{"name":"NotAllowed","type":"error","inputs":[]},{"name":"NotYourBandit","type":"error","inputs":[]},{"name":"PaidLaneDrained","type":"error","inputs":[]},{"name":"PaidLimitHit","type":"error","inputs":[]},{"name":"SettleTooEarly","type":"error","inputs":[]},{"name":"WrongCoin","type":"error","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"sent","type":"uint256","internalType":"uint256"}]},{"name":"ZeroAddress","type":"error","inputs":[]},{"name":"Approval","type":"event","inputs":[{"name":"holder","type":"address","indexed":true,"internalType":"address"},{"name":"spender","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"name":"ApprovalForAll","type":"event","inputs":[{"name":"holder","type":"address","indexed":true,"internalType":"address"},{"name":"operator","type":"address","indexed":true,"internalType":"address"},{"name":"approved","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"name":"BanditMinted","type":"event","inputs":[{"name":"member","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"viaFreeLane","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"name":"LootPaid","type":"event","inputs":[{"name":"member","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"name":"SaleSettled","type":"event","inputs":[{"name":"treasury","type":"address","indexed":true,"internalType":"address"},{"name":"bnbAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"lpAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"torchedAmount","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":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"name":"DEAD","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"LOOT_PER_MINT","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"PAID_WALLET_LIMIT","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"approve","type":"function","inputs":[{"name":"spender","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"balanceOf","type":"function","inputs":[{"name":"holder","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"baseURI","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"closesAt","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"contractURI","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"freeTakenBy","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"freeTotal","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"freeUsed","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"gate","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IAgentGate"}],"stateMutability":"view"},{"name":"getApproved","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"isApprovedForAll","type":"function","inputs":[{"name":"holder","type":"address","internalType":"address"},{"name":"operator","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"lpStash","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"mintFree","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"name":"mintPaid","type":"function","inputs":[{"name":"count","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"name":"mintedTotal","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"name","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"opensAt","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"ownerOf","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"name":"paidTakenBy","type":"function","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"paidTotal","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"paidUsed","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"priceWei","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"safeTransferFrom","type":"function","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"safeTransferFrom","type":"function","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"},{"name":"data","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"setApprovalForAll","type":"function","inputs":[{"name":"operator","type":"address","internalType":"address"},{"name":"approved","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"settle","type":"function","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"name":"settled","type":"function","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"name":"supplyCap","type":"function","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"name":"supportsInterface","type":"function","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"pure"},{"name":"swag","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ISwagBag"}],"stateMutability":"view"},{"name":"symbol","type":"function","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"tokenURI","type":"function","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"name":"transferFrom","type":"function","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"name":"treasury","type":"function","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"}],"matchId":"58738172","creationMatch":"exact_match","runtimeMatch":"exact_match","verifiedAt":"2026-11-07T11:03:05Z","match":"exact_match","chainId":"56","address":"0xa849aEd9dC19FBA823CcEf1FA01BdA72Fbb8fcDf"}