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.

03 · TapeoutReceiptDistributor

Receipt distributor

Holds the NAND and LATCH receipts until each allocated wallet claims its own. The allocation is recorded on-chain as ID ranges per wallet.

What it does

  • Bound to one custody pool, from which it reads the two receipt addresses. It refuses receipts that already have BEM claim history in that pool; with two pools sharing the receipts, the other pool's history is not checked.
  • The owner records allocations (allocate), can correct a wallet (revokeWallet), and finalizes once every receipt in inventory is allocated.
  • A beneficiary calls claim, or anyone calls deliver for them; receipts always go to the recorded wallet. There is no deadline.
  • Never sends a transaction to a pool and never approves an operator: it can only move a receipt to its beneficiary.
Privileged role
Owner: allocate, revokeWallet, finalizeAllocation, all before finalization only.
After finalization
Nothing. Beneficiaries and ID ranges are immutable.

Source code

tapeminecoop/groupmine @ 00e37c7

src/TapeoutReceiptDistributor.sol

300 lines13.8 KBSolidity 0.8.24MIT licensesha256 67f0b24fa547…

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

import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import {TapeoutEntitlementReceipt} from "./TapeoutEntitlementReceipt.sol";
import {TapeoutMinerPool} from "./TapeoutMinerPool.sol";

/// @notice Holds NAND and LATCH receipts until each allocated wallet claims its own. The operator
///         funds it with receipts, imports the allocation (tooling turns the JSON wallet counts into
///         ID ranges), and finalizes; afterwards beneficiaries and allocations are immutable.
/// @dev While held here, receipts keep their place in the pool's accounting: nobody can claim their
///      BEM (only a receipt's holder can, and this contract never calls the pool), approve them,
///      or move them anywhere except to their recorded beneficiary. Receipts that already have BEM
///      claim history are refused as inventory, and nothing can be received after finalization, so
///      previously distributed receipts can never be redistributed.
contract TapeoutReceiptDistributor is Ownable2Step, ReentrancyGuard, IERC1155Receiver {
    struct Range {
        TapeoutMinerPool.Category category;
        uint64 firstId;
        uint64 count;
        uint64 delivered;
    }

    struct AllocationInput {
        TapeoutMinerPool.Category category;
        address wallet;
        uint64 firstId;
        uint64 count;
    }

    uint256 public constant MAX_RANGE = 1000;
    uint256 public constant MAX_PAGE = 500;

    TapeoutMinerPool public immutable pool;
    TapeoutEntitlementReceipt public immutable nandReceipt;
    TapeoutEntitlementReceipt public immutable latchReceipt;

    bool public allocationFinalized;
    uint256[2] private _inventory;
    uint256[2] private _allocated;
    uint256[2] private _delivered;
    mapping(TapeoutMinerPool.Category => mapping(uint256 id => address wallet)) private _beneficiary;
    mapping(address wallet => Range[]) private _ranges;
    /// @dev Index of the wallet's first range with undelivered IDs; earlier ranges are complete.
    mapping(address wallet => uint256 index) private _nextRange;
    address[] private _wallets;
    mapping(address wallet => bool) private _listed;

    error InvalidAddress();
    error UnsupportedToken(address token);
    error AllocationClosed();
    error AllocationOpen();
    error IssuanceNotFinalized();
    error InvalidAllocation();
    error NotInInventory(TapeoutMinerPool.Category category, uint256 id);
    error AlreadyAllocated(TapeoutMinerPool.Category category, uint256 id, address wallet);
    error InventoryNotCovered(TapeoutMinerPool.Category category, uint256 allocated, uint256 inventory);
    error InvalidAmount(uint256 id, uint256 value);
    error ConsumedRewardHistory(TapeoutMinerPool.Category category, uint256 id);
    error NothingToDeliver(address wallet);
    error InvalidPage();

    event InventoryReceived(TapeoutMinerPool.Category indexed category, address indexed from, uint256 count);
    event Allocated(
        address indexed wallet, TapeoutMinerPool.Category indexed category, uint256 firstId, uint256 count
    );
    event WalletRevoked(address indexed wallet, uint256 nandCount, uint256 latchCount);
    event AllocationFinalized(uint256 nandCount, uint256 latchCount, uint256 wallets);
    event Delivered(
        address indexed wallet, TapeoutMinerPool.Category indexed category, uint256 firstId, uint256 count
    );

    constructor(address owner_, TapeoutMinerPool pool_) Ownable(owner_) {
        if (address(pool_).code.length == 0) revert InvalidAddress();
        pool = pool_;
        nandReceipt = pool_.nandReceipt();
        latchReceipt = pool_.latchReceipt();
    }

    // ---------------------------------------------------------------------------------- funding

    function onERC1155Received(address, address from, uint256 id, uint256 value, bytes calldata)
        external
        returns (bytes4)
    {
        TapeoutMinerPool.Category category = _categoryOf(msg.sender);
        if (allocationFinalized) revert AllocationClosed();
        if (_acceptOne(category, id, value)) {
            ++_inventory[uint256(category)];
            emit InventoryReceived(category, from, 1);
        }
        return IERC1155Receiver.onERC1155Received.selector;
    }

    function onERC1155BatchReceived(
        address,
        address from,
        uint256[] calldata ids,
        uint256[] calldata values,
        bytes calldata
    ) external returns (bytes4) {
        TapeoutMinerPool.Category category = _categoryOf(msg.sender);
        if (allocationFinalized) revert AllocationClosed();
        uint256 accepted;
        for (uint256 i; i < ids.length; ++i) {
            if (_acceptOne(category, ids[i], values[i])) ++accepted;
        }
        _inventory[uint256(category)] += accepted;
        emit InventoryReceived(category, from, accepted);
        return IERC1155Receiver.onERC1155BatchReceived.selector;
    }

    function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
        return interfaceId == type(IERC1155Receiver).interfaceId || interfaceId == type(IERC165).interfaceId;
    }

    // ---------------------------------------------------------------------------------- allocation

    /// @notice Record beneficiaries for ranges of IDs held as inventory. Each ID once.
    function allocate(AllocationInput[] calldata entries) external onlyOwner {
        if (allocationFinalized) revert AllocationClosed();
        if (entries.length == 0) revert InvalidAllocation();
        for (uint256 e; e < entries.length; ++e) {
            AllocationInput calldata a = entries[e];
            if (
                a.wallet == address(0) || a.wallet == address(this) || a.wallet == address(pool)
                    || a.count == 0 || a.count > MAX_RANGE || a.firstId == 0
            ) revert InvalidAllocation();
            address[] memory holders = _receipt(a.category).holdersPage(a.firstId, a.count);
            mapping(uint256 => address) storage beneficiary = _beneficiary[a.category];
            for (uint256 i; i < a.count; ++i) {
                uint256 id = uint256(a.firstId) + i;
                if (holders[i] != address(this)) revert NotInInventory(a.category, id);
                if (beneficiary[id] != address(0)) revert AlreadyAllocated(a.category, id, beneficiary[id]);
                beneficiary[id] = a.wallet;
            }
            if (!_listed[a.wallet]) {
                _listed[a.wallet] = true;
                _wallets.push(a.wallet);
            }
            _ranges[a.wallet]
            .push(Range({category: a.category, firstId: a.firstId, count: a.count, delivered: 0}));
            _allocated[uint256(a.category)] += a.count;
            emit Allocated(a.wallet, a.category, a.firstId, a.count);
        }
    }

    /// @notice Before finalization, remove every range allocated to `wallet` (to correct an import).
    function revokeWallet(address wallet) external onlyOwner {
        if (allocationFinalized) revert AllocationClosed();
        Range[] storage list = _ranges[wallet];
        uint256[2] memory removed;
        for (uint256 r; r < list.length; ++r) {
            Range storage range = list[r];
            mapping(uint256 => address) storage beneficiary = _beneficiary[range.category];
            for (uint256 i; i < range.count; ++i) {
                delete beneficiary[uint256(range.firstId) + i];
            }
            removed[uint256(range.category)] += range.count;
        }
        _allocated[0] -= removed[0];
        _allocated[1] -= removed[1];
        delete _ranges[wallet];
        emit WalletRevoked(wallet, removed[0], removed[1]);
    }

    /// @notice Freeze the allocation and open claims. Every held receipt must be allocated, and both
    ///         receipt collections must have finalized issuance.
    function finalizeAllocation() external onlyOwner {
        if (allocationFinalized) revert AllocationClosed();
        if (!nandReceipt.issuanceFinalized() || !latchReceipt.issuanceFinalized()) {
            revert IssuanceNotFinalized();
        }
        for (uint256 c; c < 2; ++c) {
            if (_allocated[c] != _inventory[c]) {
                revert InventoryNotCovered(TapeoutMinerPool.Category(c), _allocated[c], _inventory[c]);
            }
        }
        allocationFinalized = true;
        emit AllocationFinalized(_inventory[0], _inventory[1], _wallets.length);
    }

    // ---------------------------------------------------------------------------------- delivery

    /// @notice Deliver up to `maxIds` of the caller's allocated receipts to the caller.
    function claim(uint256 maxIds) external nonReentrant returns (uint256 delivered) {
        return _deliver(msg.sender, maxIds);
    }

    /// @notice Push up to `maxIds` of `wallet`'s allocated receipts to `wallet` itself. Anyone may call;
    ///         the destination is always the recorded beneficiary.
    function deliver(address wallet, uint256 maxIds) external nonReentrant returns (uint256 delivered) {
        return _deliver(wallet, maxIds);
    }

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

    function beneficiaryOf(TapeoutMinerPool.Category category, uint256 id) external view returns (address) {
        return _beneficiary[category][id];
    }

    function rangesOf(address wallet) external view returns (Range[] memory) {
        return _ranges[wallet];
    }

    /// @notice Allocated and still undelivered IDs of `wallet`, per category.
    function pendingOf(address wallet) external view returns (uint256 nandCount, uint256 latchCount) {
        Range[] storage list = _ranges[wallet];
        for (uint256 r = _nextRange[wallet]; r < list.length; ++r) {
            uint256 left = list[r].count - list[r].delivered;
            if (list[r].category == TapeoutMinerPool.Category.NAND) nandCount += left;
            else latchCount += left;
        }
    }

    /// @notice Index in `rangesOf(wallet)` of the first range with undelivered IDs (the next delivery
    ///         starts there); equal to the number of ranges once everything is delivered.
    function nextRangeOf(address wallet) external view returns (uint256) {
        return _nextRange[wallet];
    }

    function walletCount() external view returns (uint256) {
        return _wallets.length;
    }

    function walletsPage(uint256 offset, uint256 limit) external view returns (address[] memory page) {
        uint256 length = _wallets.length;
        if (limit == 0 || limit > MAX_PAGE || offset > length) revert InvalidPage();
        uint256 count = limit > length - offset ? length - offset : limit;
        page = new address[](count);
        for (uint256 i; i < count; ++i) {
            page[i] = _wallets[offset + i];
        }
    }

    /// @notice Inventory received, allocated and delivered per category ([NAND, LATCH]).
    function totals()
        external
        view
        returns (uint256[2] memory inventory, uint256[2] memory allocated, uint256[2] memory delivered)
    {
        return (_inventory, _allocated, _delivered);
    }

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

    function _categoryOf(address token) private view returns (TapeoutMinerPool.Category) {
        if (token == address(nandReceipt)) return TapeoutMinerPool.Category.NAND;
        if (token == address(latchReceipt)) return TapeoutMinerPool.Category.LATCH;
        revert UnsupportedToken(token);
    }

    function _receipt(TapeoutMinerPool.Category category) private view returns (TapeoutEntitlementReceipt) {
        return category == TapeoutMinerPool.Category.NAND ? nandReceipt : latchReceipt;
    }

    /// @dev Inventory must be fresh: a receipt whose BEM has ever been claimed is refused.
    function _acceptOne(TapeoutMinerPool.Category category, uint256 id, uint256 value)
        private
        view
        returns (bool)
    {
        if (value == 0) return false;
        if (value != 1) revert InvalidAmount(id, value);
        if (pool.claimed(category, id) != 0) revert ConsumedRewardHistory(category, id);
        return true;
    }

    /// @dev Starts at the wallet's cursor, so a delivery pays only for the ranges and IDs it transfers,
    ///      however many ranges were completed before. Every range from the cursor on has undelivered
    ///      IDs (ranges hold at least one ID and only this function delivers). All bookkeeping, the
    ///      cursor included, is written before each transfer; a failing receiver reverts all of it.
    function _deliver(address wallet, uint256 maxIds) private returns (uint256 total) {
        if (!allocationFinalized) revert AllocationOpen();
        Range[] storage list = _ranges[wallet];
        uint256 length = list.length;
        for (uint256 r = _nextRange[wallet]; r < length && total < maxIds; ++r) {
            Range storage range = list[r];
            uint256 left = range.count - range.delivered;
            uint256 n = left < maxIds - total ? left : maxIds - total;
            uint256 first = uint256(range.firstId) + range.delivered;
            range.delivered += uint64(n);
            _delivered[uint256(range.category)] += n;
            total += n;
            if (n == left) _nextRange[wallet] = r + 1;
            uint256[] memory ids = new uint256[](n);
            uint256[] memory values = new uint256[](n);
            for (uint256 i; i < n; ++i) {
                ids[i] = first + i;
                values[i] = 1;
            }
            _receipt(range.category).safeBatchTransferFrom(address(this), wallet, ids, values, "");
            emit Delivered(wallet, range.category, first, n);
        }
        if (total == 0) revert NothingToDeliver(wallet);
    }
}