Smart contracts

Contract source code

TapeMine's contracts are built from the open-source tapeminecoop/groupmine(opens in a new tab) repository. Every file below is the repository content at commit 00e37c7(opens in a new tab), checked byte-for-byte against GitHub.

02 · TapeoutEntitlementReceipt

LATCH entitlement receipt

ERC-1155 receipts for the LATCH category. Same code as the NAND receipt, deployed separately: LATCH #1 and NAND #1 are unrelated entitlements.

What it does

  • The issuer mints IDs in batches of up to 500, then calls finalizeIssuance, which is irreversible.
  • Before finalization, retract can only burn the highest IDs, and only while the issuer still holds them.
  • After finalization no mint or burn is possible, so the supply is fixed. Transfers stay ordinary ERC-1155 transfers of one ID at a time.
  • Keeps a holder index (holderOf, holdersPage) used by the custody pools to pay each ID's current holder.
Privileged role
Issuer (owner): issue, retract, finalizeIssuance, setURI.
After finalization
Supply is fixed forever. Until ownership is renounced the issuer can still change the metadata URI, never balances or supply.

Shared implementation

The NAND and LATCH receipts are two separate deployments of the same source file, TapeoutEntitlementReceipt.sol. They differ only in constructor arguments (category, name, symbol, metadata URI and issuer). The contract has no immutables, so both deployments have identical runtime code, and the miner pool only binds to receipts whose code hash and category() match.This instance: category = keccak256("TAPEOUT_LATCH_ENTITLEMENT")

Source code

tapeminecoop/groupmine @ 00e37c7

src/TapeoutEntitlementReceipt.sol

157 lines6.6 KBSolidity 0.8.24MIT licensesha256 f5657c2827ca…

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";

/// @notice Independently deployed, unique-ID ERC-1155 entitlement receipt for one category (NAND or
///         LATCH). IDs are issued sequentially as 1..S, each with a supply of exactly one; every ID is
///         an equal share of its category's income. The issuer (owner) issues in batches and then
///         finalizes; after `finalizeIssuance` no code path can mint, burn or otherwise change the
///         supply, and the issuer keeps no power over holders' receipts.
/// @dev No immutables on purpose: every deployment has the same runtime code, so the custody pool
///      can verify a receipt by code hash (`keccak256(type(TapeoutEntitlementReceipt).runtimeCode)`)
///      instead of trusting an arbitrary ERC-1155 to provide unique-ID, fixed-supply semantics.
///      The holder index follows the GroupBuyVault pattern in TapeoutMarket/test1 (commit 9d86a26).
contract TapeoutEntitlementReceipt is ERC1155, Ownable2Step {
    bytes32 public constant NAND = keccak256("TAPEOUT_NAND_ENTITLEMENT");
    bytes32 public constant LATCH = keccak256("TAPEOUT_LATCH_ENTITLEMENT");
    uint256 public constant MAX_ISSUE_BATCH = 500;
    uint256 public constant MAX_PAGE = 1000;

    bytes32 public category;
    string public name;
    string public symbol;
    bool public issuanceFinalized;

    uint256 private _supply;
    mapping(uint256 id => address holder) private _holders;

    error InvalidCategory();
    error InvalidCount();
    error IssuanceClosed();
    error NotIssuerHeld(uint256 id);
    error InvalidAmount(uint256 id, uint256 value);
    error InvalidPage();

    event Issued(address indexed to, uint256 firstId, uint256 count, uint256 supply);
    event Retracted(uint256 firstId, uint256 count, uint256 supply);
    event IssuanceFinalized(uint256 supply);
    event BaseURIUpdated(string uri);

    constructor(
        bytes32 category_,
        string memory name_,
        string memory symbol_,
        string memory uri_,
        address issuer
    ) ERC1155(uri_) Ownable(issuer) {
        if (category_ != NAND && category_ != LATCH) revert InvalidCategory();
        category = category_;
        name = name_;
        symbol = symbol_;
    }

    // ---------------------------------------------------------------------------------- issuance

    /// @notice Issue the next `count` IDs (supply+1 .. supply+count) to `to` in one batch.
    function issue(address to, uint256 count) external onlyOwner returns (uint256 firstId) {
        if (issuanceFinalized) revert IssuanceClosed();
        if (count == 0 || count > MAX_ISSUE_BATCH) revert InvalidCount();
        firstId = _supply + 1;
        uint256[] memory ids = new uint256[](count);
        uint256[] memory values = new uint256[](count);
        for (uint256 i; i < count; ++i) {
            ids[i] = firstId + i;
            values[i] = 1;
        }
        _mintBatch(to, ids, values, "");
        emit Issued(to, firstId, count, _supply);
    }

    /// @notice Before finalization, undo over-issuance: burn the highest `count` IDs, all of which
    ///         must still be held by the issuer. IDs held by anyone else can never be retracted.
    function retract(uint256 count) external onlyOwner {
        if (issuanceFinalized) revert IssuanceClosed();
        if (count == 0 || count > _supply || count > MAX_ISSUE_BATCH) revert InvalidCount();
        uint256 firstId = _supply - count + 1;
        uint256[] memory ids = new uint256[](count);
        uint256[] memory values = new uint256[](count);
        for (uint256 i; i < count; ++i) {
            ids[i] = _supply - i; // highest first, so the remaining IDs stay 1..S
            values[i] = 1;
            if (_holders[ids[i]] != msg.sender) revert NotIssuerHeld(ids[i]);
        }
        _burnBatch(msg.sender, ids, values);
        emit Retracted(firstId, count, _supply);
    }

    /// @notice Permanently fix the supply at the IDs issued so far.
    function finalizeIssuance() external onlyOwner {
        if (issuanceFinalized) revert IssuanceClosed();
        issuanceFinalized = true;
        emit IssuanceFinalized(_supply);
    }

    function setURI(string calldata uri_) external onlyOwner {
        _setURI(uri_);
        emit BaseURIUpdated(uri_);
    }

    // ---------------------------------------------------------------------------------- views

    /// @notice Number of issued IDs; the IDs are exactly 1..totalSupply().
    function totalSupply() external view returns (uint256) {
        return _supply;
    }

    function totalSupply(uint256 id) external view returns (uint256) {
        return _holders[id] == address(0) ? 0 : 1;
    }

    function exists(uint256 id) external view returns (bool) {
        return _holders[id] != address(0);
    }

    function holderOf(uint256 id) external view returns (address) {
        return _holders[id];
    }

    /// @notice Holders of IDs firstId..firstId+count-1 (zero address for IDs that do not exist).
    function holdersPage(uint256 firstId, uint256 count) external view returns (address[] memory holders) {
        if (firstId == 0 || count == 0 || count > MAX_PAGE) revert InvalidPage();
        holders = new address[](count);
        for (uint256 i; i < count; ++i) {
            holders[i] = _holders[firstId + i];
        }
    }

    // ---------------------------------------------------------------------------------- internals

    function _update(address from, address to, uint256[] memory ids, uint256[] memory values)
        internal
        override
    {
        bool minting = from == address(0);
        bool burning = to == address(0);
        if ((minting || burning) && issuanceFinalized) revert IssuanceClosed();
        for (uint256 i; i < ids.length; ++i) {
            uint256 id = ids[i];
            uint256 value = values[i];
            if (value > 1) revert InvalidAmount(id, value);
            if (value == 0) continue;
            if (minting) {
                // Only `issue` mints, and only the next sequential IDs.
                if (id != _supply + 1) revert InvalidAmount(id, value);
                ++_supply;
            } else if (burning) {
                // Only `retract` burns, and only from the top.
                if (id != _supply) revert InvalidAmount(id, value);
                --_supply;
            }
            // Balance checks happen in super._update; any inconsistency reverts the whole call.
            _holders[id] = to;
        }
        super._update(from, to, ids, values);
    }
}