ERC-721 Consecutive

Consecutive extension for ERC-721 is useful for efficiently minting multiple tokens in a single transaction. This can significantly reduce gas costs and improve performance when creating a large number of tokens at once.

Usage

In order to make ERC-721 Consecutive methods “external” so that other contracts can call them, you need to add the following code to your contract:

use openzeppelin_stylus::token::erc721::extensions::consecutive::{
    Erc721Consecutive, Error,
};

sol_storage! {
    #[entrypoint]
    struct Erc721ConsecutiveExample {
        #[borrow]
        Erc721Consecutive erc721_consecutive;
    }
}

#[public]
#[inherit(Erc721Consecutive)]
impl Erc721ConsecutiveExample {
    pub fn burn(&mut self, token_id: U256) -> Result<(), Error> {
        self.erc721_consecutive._burn(token_id)
    }

    pub fn mint(&mut self, to: Address, token_id: U256) -> Result<(), Error> {
        self.erc721_consecutive._mint(to, token_id)
    }
}

Additionally, you need to ensure proper initialization during contract deployment. Make sure to include the following code in your Solidity Constructor:

contract Erc721ConsecutiveExample {
    mapping(uint256 tokenId => address) private _owners;
    mapping(address owner => uint256) private _balances;
    mapping(uint256 tokenId => address) private _tokenApprovals;
    mapping(address owner => mapping(address operator => bool)) private _operatorApprovals;

    Checkpoint160[] private _checkpoints; // _sequentialOwnership
    mapping(uint256 bucket => uint256) private _data; // _sequentialBurn
    uint96 private _firstConsecutiveId;
    uint96 private _maxBatchSize;

    error ERC721InvalidReceiver(address receiver);
    error ERC721ForbiddenBatchMint();
    error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch);
    error ERC721ForbiddenMint();
    error ERC721ForbiddenBatchBurn();
    error CheckpointUnorderedInsertion();

    event ConsecutiveTransfer(
        uint256 indexed fromTokenId,
        uint256 toTokenId,
        address indexed fromAddress,
        address indexed toAddress
    );

    struct Checkpoint160 {
        uint96 _key;
        uint160 _value;
    }

    constructor(
        address[] memory receivers,
        uint96[] memory amounts,
        uint96 firstConsecutiveId,
        uint96 maxBatchSize)
    {
        _firstConsecutiveId = firstConsecutiveId;
        _maxBatchSize = maxBatchSize;
        for (uint256 i = 0; i < receivers.length; ++i) {
            _mintConsecutive(receivers[i], amounts[i]);
        }
    }

    function latestCheckpoint() internal view returns (bool exists, uint96 _key, uint160 _value) {
        uint256 pos = _checkpoints.length;
        if (pos == 0) {
            return (false, 0, 0);
        } else {
            Checkpoint160 storage ckpt = _checkpoints[pos - 1];
            return (true, ckpt._key, ckpt._value);
        }
    }

    function push(uint96 key, uint160 value) internal returns (uint160, uint160) {
        return _insert(key, value);
    }

    function _insert(uint96 key, uint160 value) private returns (uint160, uint160) {
        uint256 pos = _checkpoints.length;

        if (pos > 0) {
            Checkpoint160 storage last = _checkpoints[pos - 1];
            uint96 lastKey = last._key;
            uint160 lastValue = last._value;

            // Checkpoint keys must be non-decreasing.
            if (lastKey > key) {
                revert CheckpointUnorderedInsertion();
            }

            // Update or push new checkpoint.
            if (lastKey == key) {
                _checkpoints[pos - 1]._value = value;
            } else {
                _checkpoints.push(Checkpoint160({_key: key, _value: value}));
            }
            return (lastValue, value);
        } else {
            _checkpoints.push(Checkpoint160({_key: key, _value: value}));
            return (0, value);
        }
    }

    function _mintConsecutive(address to, uint96 batchSize) internal virtual returns (uint96) {
        uint96 next = _nextConsecutiveId();

        // minting a batch of size 0 is a no-op.
        if (batchSize > 0) {
            if (address(this).code.length > 0) {
                revert ERC721ForbiddenBatchMint();
            }
            if (to == address(0)) {
                revert ERC721InvalidReceiver(address(0));
            }

            uint256 maxBatchSize = _maxBatchSize;
            if (batchSize > maxBatchSize) {
                revert ERC721ExceededMaxBatchMint(batchSize, maxBatchSize);
            }

            // push an ownership checkpoint & emit event.
            uint96 last = next + batchSize - 1;
            push(last, uint160(to));

            // The invariant required by this function is preserved because the new sequentialOwnership checkpoint
            // is attributing ownership of `batchSize` new tokens to account `to`.
            _increaseBalance(to, batchSize);

            emit ConsecutiveTransfer(next, last, address(0), to);
        }

        return next;
    }

    function _nextConsecutiveId() private view returns (uint96) {
        (bool exists, uint96 latestId,) = latestCheckpoint();
        return exists ? latestId + 1 : _firstConsecutiveId;
    }

    function _increaseBalance(address account, uint128 value) internal virtual {
        unchecked {
            _balances[account] += value;
        }
    }
}