{"sources":{"contracts/Unbroken.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.24;\n\n/// UNBROKEN, second contract. Not deployed yet.\n/// Ten thousand pieces, each one unbroken line around a still centre.\n///\n/// Rules:\n///  - pieces 0 to 2499 are free for wallets that hold a .ai Agent ID, two per wallet\n///  - from 2500 the price steps in USD and is settled in BNB through Chainlink\n///  - the public mint opens only when the owner calls openMint(), and can be shut again\n///  - royalty 5 percent through ERC-2981, paid into this contract and withdrawn by the owner\n///  - no proxy, no upgrade path\ncontract Unbroken {\n    string public constant name = \"UNBROKEN\";\n    string public constant symbol = \"UBK\";\n\n    uint256 public constant MAX_SUPPLY = 10000;\n    uint256 public constant FREE_TIER = 2500;\n    uint256 public constant MAX_FREE_PER_WALLET = 2;\n    uint256 public constant MAX_PER_TX = 10;\n    uint256 public constant MAX_OWNER_MINT = 200;\n    uint256 public constant ROYALTY_BPS = 500;\n\n    /// .ai Agent ID registry on BNB Chain.\n    address public immutable REGISTRY;\n    /// Chainlink BNB / USD feed, 8 decimals.\n    address public immutable FEED;\n\n    address public owner;\n    string public baseURI;\n\n    /// Public mint state. Shut on deploy, opened only by the owner.\n    bool public mintOpen;\n    /// Emergency stop. Holds even when mintOpen is true.\n    bool public paused;\n\n    uint256 public totalMinted;\n    mapping(address => uint256) public freeMintedBy;\n\n    mapping(uint256 => address) private _ownerOf;\n    mapping(address => uint256) private _balanceOf;\n    mapping(uint256 => address) private _approved;\n    mapping(address => mapping(address => bool)) private _operator;\n\n    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\n    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\n    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\n    event OwnerChanged(address indexed from, address indexed to);\n    event BaseURIChanged(string uri);\n    event MintOpenChanged(bool open);\n    event PausedChanged(bool paused);\n    event Withdrawn(address indexed to, uint256 amount);\n\n    error NotOwner();\n    error SoldOut();\n    error BadQuantity();\n    error MintClosed();\n    error IsPaused();\n    error NoAgentId();\n    error FreeQuotaUsed();\n    error Underpaid(uint256 required);\n    error TransferFailed();\n    error ZeroAddress();\n    error NotTokenOwner();\n    error NotAuthorized();\n    error BadReceiver();\n\n    modifier onlyOwner() {\n        if (msg.sender != owner) revert NotOwner();\n        _;\n    }\n\n    constructor(address owner_, string memory baseURI_, address registry_, address feed_) {\n        if (owner_ == address(0) || registry_ == address(0) || feed_ == address(0)) revert ZeroAddress();\n        owner = owner_;\n        baseURI = baseURI_;\n        REGISTRY = registry_;\n        FEED = feed_;\n        mintOpen = false;\n        paused = false;\n    }\n\n    // ------------------------------------------------------------- pricing\n\n    /// Price in USD with 8 decimals for the piece at this index on the ladder.\n    function priceAt(uint256 index) public pure returns (uint256) {\n        if (index < 2500) return 0;\n        if (index < 5000) return 25e6;\n        if (index < 7500) return 50e6;\n        return 75e6;\n    }\n\n    function currentPriceUsd() public view returns (uint256) {\n        return priceAt(totalMinted);\n    }\n\n    function bnbPriceUsd() public view returns (uint256) {\n        (, int256 answer, , , ) = IAggregator(FEED).latestRoundData();\n        require(answer > 0, \"feed\");\n        return uint256(answer);\n    }\n\n    function priceBnb() public view returns (uint256) {\n        uint256 usd = currentPriceUsd();\n        if (usd == 0) return 0;\n        return (usd * 1e18) / bnbPriceUsd();\n    }\n\n    /// Total BNB needed to mint qty pieces right now.\n    function cost(uint256 qty) public view returns (uint256) {\n        uint256 usd = 0;\n        uint256 minted = totalMinted;\n        for (uint256 i = 0; i < qty; i++) {\n            uint256 at = minted + i;\n            if (at >= MAX_SUPPLY) break;\n            usd += priceAt(at);\n        }\n        if (usd == 0) return 0;\n        return (usd * 1e18) / bnbPriceUsd();\n    }\n\n    // ---------------------------------------------------------- agent gate\n\n    /// True when the wallet holds a registered .ai Agent ID.\n    function hasAgentId(address wallet) public view returns (bool) {\n        try IRegistry(REGISTRY).handleOf(wallet) returns (string memory handle) {\n            if (bytes(handle).length == 0) return false;\n            uint256 agentId = uint256(keccak256(bytes(handle)));\n            try IRegistry(REGISTRY).ownerOf(agentId) returns (address holder) {\n                return holder == wallet;\n            } catch {\n                return false;\n            }\n        } catch {\n            return false;\n        }\n    }\n\n    function freeLeft(address wallet) public view returns (uint256) {\n        uint256 used = freeMintedBy[wallet];\n        if (used >= MAX_FREE_PER_WALLET) return 0;\n        return MAX_FREE_PER_WALLET - used;\n    }\n\n    // --------------------------------------------------------------- mint\n\n    function mint(uint256 qty) external payable {\n        if (paused) revert IsPaused();\n        if (!mintOpen) revert MintClosed();\n        if (qty == 0 || qty > MAX_PER_TX) revert BadQuantity();\n\n        uint256 minted = totalMinted;\n        if (minted + qty > MAX_SUPPLY) revert SoldOut();\n\n        uint256 freeQty;\n        for (uint256 i = 0; i < qty; i++) {\n            if (minted + i < FREE_TIER) freeQty++;\n        }\n\n        if (freeQty > 0) {\n            if (!hasAgentId(msg.sender)) revert NoAgentId();\n            if (freeMintedBy[msg.sender] + freeQty > MAX_FREE_PER_WALLET) revert FreeQuotaUsed();\n            freeMintedBy[msg.sender] += freeQty;\n        }\n\n        uint256 need = cost(qty);\n        if (msg.value < need) revert Underpaid(need);\n\n        for (uint256 i = 0; i < qty; i++) {\n            _mint(msg.sender, minted + i);\n        }\n\n        uint256 refund = msg.value - need;\n        if (refund > 0) {\n            (bool ok, ) = msg.sender.call{ value: refund }(\"\");\n            if (!ok) revert TransferFailed();\n        }\n    }\n\n    /// Owner mint for seeding a wallet. Works while the public mint is shut.\n    function ownerMint(uint256 qty, address to) external onlyOwner {\n        if (qty == 0 || qty > MAX_OWNER_MINT) revert BadQuantity();\n        if (to == address(0)) revert ZeroAddress();\n        uint256 minted = totalMinted;\n        if (minted + qty > MAX_SUPPLY) revert SoldOut();\n        for (uint256 i = 0; i < qty; i++) {\n            _mint(to, minted + i);\n        }\n    }\n\n    // ------------------------------------------------------------- ERC-721\n\n    function balanceOf(address wallet) external view returns (uint256) {\n        return _balanceOf[wallet];\n    }\n\n    function ownerOf(uint256 tokenId) public view returns (address) {\n        address holder = _ownerOf[tokenId];\n        if (holder == address(0)) revert NotTokenOwner();\n        return holder;\n    }\n\n    function getApproved(uint256 tokenId) external view returns (address) {\n        return _approved[tokenId];\n    }\n\n    function isApprovedForAll(address holder, address operator) external view returns (bool) {\n        return _operator[holder][operator];\n    }\n\n    function approve(address to, uint256 tokenId) external {\n        address holder = ownerOf(tokenId);\n        if (msg.sender != holder && !_operator[holder][msg.sender]) revert NotAuthorized();\n        _approved[tokenId] = to;\n        emit Approval(holder, to, tokenId);\n    }\n\n    function setApprovalForAll(address operator, bool approved) external {\n        _operator[msg.sender][operator] = approved;\n        emit ApprovalForAll(msg.sender, operator, approved);\n    }\n\n    function transferFrom(address from, address to, uint256 tokenId) public {\n        _transfer(from, to, tokenId);\n    }\n\n    function safeTransferFrom(address from, address to, uint256 tokenId) external {\n        _transfer(from, to, tokenId);\n        _checkReceiver(from, to, tokenId, \"\");\n    }\n\n    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external {\n        _transfer(from, to, tokenId);\n        _checkReceiver(from, to, tokenId, data);\n    }\n\n    function supportsInterface(bytes4 id) external pure returns (bool) {\n        return id == 0x01ffc9a7            // ERC-165\n            || id == 0x80ac58cd            // ERC-721\n            || id == 0x5b5e139f            // ERC-721 metadata\n            || id == 0x2a55205a;           // ERC-2981\n    }\n\n    function tokenURI(uint256 tokenId) external view returns (string memory) {\n        ownerOf(tokenId);\n        return string.concat(baseURI, _toString(tokenId), \".json\");\n    }\n\n    // ------------------------------------------------------------ ERC-2981\n\n    /// Royalty is held by this contract and withdrawn by the owner.\n    function royaltyInfo(uint256, uint256 salePrice) external view returns (address, uint256) {\n        return (address(this), (salePrice * ROYALTY_BPS) / 10000);\n    }\n\n    // -------------------------------------------------------------- owner\n\n    function setBaseURI(string calldata uri) external onlyOwner {\n        baseURI = uri;\n        emit BaseURIChanged(uri);\n    }\n\n    /// Opens or shuts the public mint. Pause still overrides this.\n    function setMintOpen(bool open) external onlyOwner {\n        mintOpen = open;\n        emit MintOpenChanged(open);\n    }\n\n    /// Emergency stop for the public mint. Owner mint keeps working.\n    function setPaused(bool value) external onlyOwner {\n        paused = value;\n        emit PausedChanged(value);\n    }\n\n    function transferOwnership(address next) external onlyOwner {\n        if (next == address(0)) revert ZeroAddress();\n        emit OwnerChanged(owner, next);\n        owner = next;\n    }\n\n    function withdraw(address to) external onlyOwner {\n        if (to == address(0)) revert ZeroAddress();\n        uint256 amount = address(this).balance;\n        (bool ok, ) = to.call{ value: amount }(\"\");\n        if (!ok) revert TransferFailed();\n        emit Withdrawn(to, amount);\n    }\n\n    // ----------------------------------------------------------- internal\n\n    function _transfer(address from, address to, uint256 tokenId) private {\n        address holder = ownerOf(tokenId);\n        if (holder != from) revert NotTokenOwner();\n        if (to == address(0)) revert ZeroAddress();\n        if (msg.sender != holder && !_operator[holder][msg.sender] && _approved[tokenId] != msg.sender) {\n            revert NotAuthorized();\n        }\n        delete _approved[tokenId];\n        _balanceOf[from] -= 1;\n        _balanceOf[to] += 1;\n        _ownerOf[tokenId] = to;\n        emit Transfer(from, to, tokenId);\n    }\n\n    function _checkReceiver(address from, address to, uint256 tokenId, bytes memory data) private {\n        if (to.code.length == 0) return;\n        try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 ret) {\n            if (ret != IERC721Receiver.onERC721Received.selector) revert BadReceiver();\n        } catch {\n            revert BadReceiver();\n        }\n    }\n\n    function _mint(address to, uint256 tokenId) private {\n        if (to == address(0)) revert ZeroAddress();\n        _balanceOf[to] += 1;\n        _ownerOf[tokenId] = to;\n        totalMinted += 1;\n        emit Transfer(address(0), to, tokenId);\n    }\n\n    function _toString(uint256 value) private pure returns (string memory) {\n        if (value == 0) return \"0\";\n        uint256 temp = value;\n        uint256 digits;\n        while (temp != 0) { digits++; temp /= 10; }\n        bytes memory buffer = new bytes(digits);\n        while (value != 0) {\n            digits -= 1;\n            buffer[digits] = bytes1(uint8(48 + (value % 10)));\n            value /= 10;\n        }\n        return string(buffer);\n    }\n\n    receive() external payable {}\n}\n\ninterface IRegistry {\n    function handleOf(address wallet) external view returns (string memory);\n    function ownerOf(uint256 agentId) external view returns (address);\n}\n\ninterface IAggregator {\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80);\n}\n\ninterface IERC721Receiver {\n    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data)\n        external\n        returns (bytes4);\n}\n"}},"matchId":"57241273","creationMatch":"exact_match","runtimeMatch":"exact_match","verifiedAt":"2026-10-27T12:21:50Z","match":"exact_match","chainId":"56","address":"0x0A091f43A082550fb73D9A6c63D39d6DcE382Bc3"}