Openzeppelin Smart Contract Solidity

OpenzeppelinのERC721の拡張機能を理解して使えるようになろう!

かるでね

こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

CryptoGamesは、「クリプトスペルズ」をはじめとするブロックチェーンゲームの開発・運営や、ブロックチェーン関連の開発を行っている会社です。

現在エンジニアを中心に積極採用中ですので、少しでも興味を持った方はぜひお話しだけでもできたら嬉しいです!

以下のWantedlyのサイトで会社について詳しく知ることができます!

以下のストーリーでは、実際にCryptoGamesで働いているメンバーのリアルな声を知ることができます。

ぜひ上記Wantedlyからか、以下のCardeneのTwitter などから連絡してください!

今回の記事ではERC721の拡張機能を1つずつ紹介していきます!

以下のOpenzeppelin内の拡張機能をもとに紹介していきます。

ERC721の拡張機能は以下の7つあります。

拡張機能

  • ERC721Pausable
  • ERC721Burnable
  • ERC721Consecutive
  • ERC721URIStorage
  • ERC721Votes
  • ERC721Royalty
  • ERC721Wrapper

結構長いので、それぞれの拡張機能を使用するときに、「どんな実装がされているのか確認する」などの使い方をしてください。

また、今回の内容を簡単にまとめたものを以下の勉強会で発表しました。

前置きは早々に中身を見ていきましょう!

ERC721とは?

そもそもERC721って何?」という方もいると思います。

ERC721については以下の記事を参考にしてください!

ERC721Pausable

コントラクトの機能を一時停止にする拡張機能。

NFTを新たに発行したり、他のアドレスに送付する機能をオン/オフできるERC721の拡張機能です。

具体的な実装コードは以下になります。

Pausable.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../utils/Context.sol";

abstract contract Pausable is Context {
    event Paused(address account);

    event Unpaused(address account);

    bool private _paused;

    constructor() {
        _paused = false;
    }

    modifier whenNotPaused() {
        _requireNotPaused();
        _;
    }

    modifier whenPaused() {
        _requirePaused();
        _;
    }

    function paused() public view virtual returns (bool) {
        return _paused;
    }

    function _requireNotPaused() internal view virtual {
        require(!paused(), "Pausable: paused");
    }

    function _requirePaused() internal view virtual {
        require(paused(), "Pausable: not paused");
    }

    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(_msgSender());
    }

    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(_msgSender());
    }
}

Paused (Event)

ERC721コントラクトのNFTの作成や送付機能が一時停止された時に発行されるログ。

Unpaused (Event)

ERC721コントラクトのNFTの作成や送付機能の一時停止が解除された時に発行されるログ。

whenNotPaused (modifier)

ERC721コントラクトのNFTの作成や送付機能が一時停止されていないかチェックする修飾子。

一時停止されていない時だけ実行する。

修飾子とは、関数につけることができるオプション機能で、関数実行前に特定のチェックをしてくれます。

whenPaused

ERC721コントラクトのNFTの作成や送付機能が一時停止かチェックする修飾子。

一時停止されている時だけ実行する。

paused

ERC721コントラクトが現在一時停止されているか確認する関数。

_requireNotPaused

ERC721コントラクトが停止されていたらエラーを返す関数。

_requirePaused

ERC721コントラクトが停止されていなかったらエラーを返す関数。

_pause

ERC721コントラクトを一時停止する関数。

すでに一時停止されていれば実行しない。

_unpause

ERC721コントラクトの一時停止を解除する関数。

一時停止されていなければ実行しない。

ERC721Pausable.sol

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC721/extensions/ERC721Pausable.sol)

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "../../../security/Pausable.sol";

abstract contract ERC721Pausable is ERC721, Pausable {
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 firstTokenId,
        uint256 batchSize
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, firstTokenId, batchSize);

        require(!paused(), "ERC721Pausable: token transfer while paused");
    }
}

_beforeTokenTransfer

この関数はERC721コントラクトのNFTの作成や送付機能を実行する前に呼ばれる関数です。

そのため、処理を実行する前に何かしらチェックするのに役立ちます。

先ほどのPausable.solをインポートして使用しています。

ERC721Burnable

特定のNFTを破棄する拡張機能。

既に発行済みのERC721規格のNFTを破棄する関数です。

この機能は拡張機能ですが、OpenzeppelinではERC721.solで実装されています。

ERC721Burnable.sol

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Burnable.sol)

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "../../../utils/Context.sol";

abstract contract ERC721Burnable is Context, ERC721 {
    function burn(uint256 tokenId) public virtual {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
        _burn(tokenId);
    }
}

burn

ERC721規格のNFTの所有者、もしくは所有者から権限を与えられたアドレスからの実行かを確認しています。

確認の結果がtrueであれば、ERC721.sol内の_burn関数を実行しています。

ERC721.sol

...
function _burn(uint256 tokenId) internal virtual {
        address owner = ERC721.ownerOf(tokenId);

        _beforeTokenTransfer(owner, address(0), tokenId, 1);

        owner = ERC721.ownerOf(tokenId);

        delete _tokenApprovals[tokenId];

        unchecked {
            _balances[owner] -= 1;
        }
        delete _owners[tokenId];

        emit Transfer(owner, address(0), tokenId);

        _afterTokenTransfer(owner, address(0), tokenId, 1);
    }
...

_burn

破棄したいERC721規格のNFTのトークンIDを使用して、所有者のアドレスを取得しています。

トークンIDとは、NFTを識別するユニークな値です。「1」や「2」など数字が使われることが多いです。

その後、_beforeTokenTransfer関数を呼びだし、何かしら処理が書かれていたらそれを実行します。

なんらかの処理で所有者が変わっている可能性もあるため、再度破棄したいNFTの所有者のアドレスを取得しています。

破棄したいNFTの操作権限を与えられているアドレスの許可を外します。

破棄したいNFTの所有者の保有NFT量を-1して、所有しているという記録も削除します。

最後にTransferイベントを発行して、_afterTokenTransfer関数を実行して何かしらの処理が書かれていたらそれを実行します。

ERC721Consecutive

一度に複数のERC721規格のNFTを発行する拡張機能。

ERC721規格のNFTをコントラクトのデプロイ時のみ一度に複数発行することができる拡張機能です。

ERC2309での提案をもとに作成されています。

ERC2309については以下を参考にしてください。

注意点としては、この拡張機能をが実行されているとき、単一のERC721規格のNFTの発行ができなくなることです。

このERC721Consectiveはコントラクトのデプロイ時にのみ実行できます。

上記の理由としては、「連続したトークンID」を維持するために、他のMint(生成)処理が走らないようにしているためだからです。

ERC721Consecutive.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "../../../interfaces/IERC2309.sol";
import "../../../utils/Checkpoints.sol";
import "../../../utils/structs/BitMaps.sol";

abstract contract ERC721Consecutive is IERC2309, ERC721 {
    using BitMaps for BitMaps.BitMap;
    using Checkpoints for Checkpoints.Trace160;

    Checkpoints.Trace160 private _sequentialOwnership;
    BitMaps.BitMap private _sequentialBurn;

    function _maxBatchSize() internal view virtual returns (uint96) {
        return 5000;
    }

    function _ownerOf(uint256 tokenId) internal view virtual override returns (address) {
        address owner = super._ownerOf(tokenId);

        if (owner != address(0) || tokenId > type(uint96).max) {
            return owner;
        }

        return _sequentialBurn.get(tokenId) ? address(0) : address(_sequentialOwnership.lowerLookup(uint96(tokenId)));
    }

    function _mintConsecutive(address to, uint96 batchSize) internal virtual returns (uint96) {
        uint96 first = _totalConsecutiveSupply();

        if (batchSize > 0) {
            require(!Address.isContract(address(this)), "ERC721Consecutive: batch minting restricted to constructor");
            require(to != address(0), "ERC721Consecutive: mint to the zero address");
            require(batchSize <= _maxBatchSize(), "ERC721Consecutive: batch too large");

            _beforeTokenTransfer(address(0), to, first, batchSize);

            uint96 last = first + batchSize - 1;
            _sequentialOwnership.push(last, uint160(to));

            __unsafe_increaseBalance(to, batchSize);

            emit ConsecutiveTransfer(first, last, address(0), to);

            _afterTokenTransfer(address(0), to, first, batchSize);
        }

        return first;
    }

    function _mint(address to, uint256 tokenId) internal virtual override {
        require(Address.isContract(address(this)), "ERC721Consecutive: can't mint during construction");
        super._mint(to, tokenId);
    }

    function _afterTokenTransfer(
        address from,
        address to,
        uint256 firstTokenId,
        uint256 batchSize
    ) internal virtual override {
        if (
            to == address(0) && // if we burn
            firstTokenId < _totalConsecutiveSupply() && // and the tokenId was minted in a batch
            !_sequentialBurn.get(firstTokenId) // and the token was never marked as burnt
        ) {
            require(batchSize == 1, "ERC721Consecutive: batch burn not supported");
            _sequentialBurn.set(firstTokenId);
        }
        super._afterTokenTransfer(from, to, firstTokenId, batchSize);
    }

    function _totalConsecutiveSupply() private view returns (uint96) {
        (bool exists, uint96 latestId, ) = _sequentialOwnership.latestCheckpoint();
        return exists ? latestId + 1 : 0;
    }
}

_maxBatchSize

一度に発行できるERC721規格のNFTの最大数を取得する関数。

NFTを発行するときに最大値を設定していないと、やたら多くの枚数を発行される可能性があります。

その影響で本来NFTを受け取れたはずのアドレスが受け取れなかったり、トランザクションが通らなくなったりなどの問題が起こります。

_ownerOf

特定のERC721規格のNFTの所有者を返す関数。

所有者が0アドレス、もしくは引数で渡しているtokenIduint96の範囲内であれば、NFTの所有者のアドレスを返します。

_mintConsecutive部分で詳細は説明しますが、複数NFTを一度に発行するとき先頭のトークンIDのみコントラクトにデータをして保存しています。
そのため、先頭以外のNFTの所有者は0アドレスになってしまっているので、それを取得する処理が最後に実行されています。

なぜuint256ではなくuint96の範囲内かのチェックなのかというと、uint256だと数値が大きすぎるのでストレージやガスコストを最小限に抑えているためです。

もし、先ほどの条件に一致しない場合は、指定したtokenIdのNFTが破棄されていないか確認したのち、NFTの所有者アドレスを返します。

_mintConsecutive

複数のERC721規格のNFTを発行する関数。

この関数を実行するにあたり、以下の条件があります。

条件

  • 引数のbatchSizeが一度に発行できるNFTの最大数である、_maxBatchSizeを超えていないか確認する必要があります。
  • コントラクトデプロイ時に一度実行されるconstructor内で実行される必要がある。
  • constructor内で実行されるため、Transferイベントを発行しません。
  • 送り先がコントラクトアドレスか確認するonERC721Receivedを呼び出しません。
  • ERC2309規格で定義されているConsecutiveTransferイベントを発行します。

上記の条件をもとに以下の処理を実行しています。

_totalConsecutiveSupply()関数を呼び出して、現在までに発行されているNFTの総数を取得してfirstという変数に格納します。

1引数のbatchSizeが0の場合はfirstの値のみ返して処理を終了します。

引数のbatchSizeが1以上の場合、以下の3つを確認します。

チェック項目

  • constructorからの実行か?
  • 発行先のアドレスが0アドレスではないか?
  • batchSize_maxBatchSize以下であるか?

_beforeTokenTransferを呼び出して、何かしら処理が書かれていたら実行します。

_sequentialOwnershipという構造体に複数発行したNFTの最後のtokenId + 1の値を格納し、指定した範囲内のNFTの所有権を記録します。

構造体とは以下のようなものです。

struct User {
    string name;
    address owner;
    uint256 id;  
}

__unsafe_increaseBalanceを実行して、新規発行したNFTの所有者の所有NFT数を増加させます。

ConsecutiveTransferイベントを発行しログとして記録します。

最後に_afterTokenTransferを呼び出して、なんらかの処理が書かれていたら実行し、呼び出しもとに複数発行したNFTの最初のトークンIDであるfirstの値を返します。

_mint

単一のERC721規格のNFTを発行する関数。

コントラクトのデプロイ時には優先して_mintConsecutiveが実行されるため、_mintは使用できなくなります。

一方、コントラクトのデプロイ後は_mintConsecutiveを実行することはできなくなり、_mintを実行できるようになります。

_afterTokenTransfer

ERC721規格のNFTを発行したり、送付した後に呼び出される関数。

以下の条件が満たされているとき、tokenIdで指定されたNFTを破棄します。

条件

  • toのアドレスが0アドレスである。
  • firstTokenIdの値がNFTの最大発行数よりも小さく、一度に複数発行されたNFTである。
  • firstTokenIdがすでに破棄されたNFTではない。
  • batchSizeの値が1になっている。

上記の条件が満たされているとき、_sequentialBurnという構造体に破棄したというマークをつけます。

最後にsuper._afterTokenTransferを呼び出して、親クラスの_afterTokenTransferを実行します。

_totalConsecutiveSupply

発行されたERC721規格のNFTの総量 + 1の値を返す関数。

_sequentialOwnership.latestCheckpoint()を呼び出して、_sequentialOwnershipという構造体の最新のデータを取得します。

そもそもデータ存在するかの情報(exists)と最新のトークンID(latestId)が返されます。

existstrueの場合、最新のトークンIDlatestId1を加えた発行されたNFTの総量である値を返します。

existsfalseの場合、発行量として0を返します。

ERC721URIStorage

コントラクト内にURIデータを保存する拡張機能。

コントラクト内にtokenURIのデータを保存することができる拡張機能です。

tokenURIとは、NFTに紐付けられたメタデータなどの情報を参照するためのhttps://などから始まる、通常文字列として表現されるURLなどが格納されています。
ERC721では、NFTの名前、説明、所有者アドレス、画像などのリンクをメタデータとして保存しています。

ERC721URIStorage

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "../../../interfaces/IERC4906.sol";

abstract contract ERC721URIStorage is IERC4906, ERC721 {
    using Strings for uint256;

    mapping(uint256 => string) private _tokenURIs;

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) {
        return interfaceId == bytes4(0x49064906) || super.supportsInterface(interfaceId);
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        _requireMinted(tokenId);

        string memory _tokenURI = _tokenURIs[tokenId];
        string memory base = _baseURI();

        if (bytes(base).length == 0) {
            return _tokenURI;
        }
        if (bytes(_tokenURI).length > 0) {
            return string(abi.encodePacked(base, _tokenURI));
        }
        return super.tokenURI(tokenId);
    }

    function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
        require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token");
        _tokenURIs[tokenId] = _tokenURI;

        emit MetadataUpdate(tokenId);
    }

    function _burn(uint256 tokenId) internal virtual override {
        super._burn(tokenId);

        if (bytes(_tokenURIs[tokenId]).length != 0) {
            delete _tokenURIs[tokenId];
        }
    }
}

_tokenURIs

それぞれのERC721規格のNFTのURIを紐づけている配列。

supportsInterface

特定のコントラクトがERC4906をサポートしているかチェックする関数。

interfaceIdという引数と0x49064906という値が等しいかどうか確認しています。

等しい場合はtrueを返してERC4906をサポートしていることを表し、異なる場合は親クラスであるIERC165supportsInterfaceを呼び出します。

tokenURI

特定のERC721規格のNFTのトークンURIを取得する関数。

_requireMintedを実行して、引数で渡されたtokenIdが存在するか確認します。

存在すれば、tokneIdのトークンURIと_baseURI関数を実行してベースURIを取得します。

もしベースURIが設定されていなければ、トークンURIを呼び出しもとに返します。

ベースURIとトークンURIが両方を連結したURIを呼び出しもとに返します。

ベースURIとトークンURIの両方とも存在しないときは、親クラスであるsuper.tokenURI関数を呼び出します。

_setTokenURI

特定のERC721規格のNFTのトークンURIを設定・変更する関数。

引数で渡されたtokenIdが存在するか確認します。

存在すれば、引数で渡された_tokenURItokenIdのNFTのメタデータとして設定・更新します。

_burn

特定のERC721規格のNFTを破棄する関数。

引数で渡されtokenIdを使用して、親クラスの_burn関数を呼び出してNFTの破棄を実行します。

その後、破棄したNFTに紐づくトークンURIが存在するか確認しています。

_tokenURIs[tokenId]の長さが0でない場合、トークンURIが設定されていると判断します。

もしトークンURIが存在すれば、_tokenURIs配列からトークンURIを削除します。

このように処理を実行することで、特定のNFTが破棄されたときに紐づけられたトークンURIも同時に破棄することができます。

ERC721Votes

NFT1つにつき自分、もしくは他のアドレスに委任を1回することができる拡張機能。

NFT1つにつき投票権が1つあるとし、自分、もしくは他のアドレスに対して投票権を委任をすることができる。

委任とは、「あるアドレスに対して投票権を渡してアクティブにする」と置き換えてもらっても問題ないです。

委任のたびに追加のコストがかかるため、委任するまでは投票としてカウントされません。

アクティブにするとは、「投票権を有効化する」ということです。
アクティブになっていない投票権は投票に使用できません。

このようにNFTの保有者がガバナンスの意思決定に参加することができるようになるなど、ERC721規格のNFTを利用した投票権の委任や各アドレスの委任履歴を管理するメカニズムを提供する拡張機能です。

投票イベントや投票自体は別のコントラクトで実装するような拡張機能になっています。

Votes.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../../interfaces/IERC5805.sol";
import "../../utils/Context.sol";
import "../../utils/Counters.sol";
import "../../utils/Checkpoints.sol";
import "../../utils/cryptography/EIP712.sol";

abstract contract Votes is Context, EIP712, IERC5805 {
    using Checkpoints for Checkpoints.Trace224;
    using Counters for Counters.Counter;

    bytes32 private constant _DELEGATION_TYPEHASH =
        keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

    mapping(address => address) private _delegation;

    mapping(address => Checkpoints.Trace224) private _delegateCheckpoints;

    Checkpoints.Trace224 private _totalCheckpoints;

    mapping(address => Counters.Counter) private _nonces;

    function clock() public view virtual override returns (uint48) {
        return SafeCast.toUint48(block.number);
    }

    function CLOCK_MODE() public view virtual override returns (string memory) {
        require(clock() == block.number, "Votes: broken clock mode");
        return "mode=blocknumber&from=default";
    }

    function getVotes(address account) public view virtual override returns (uint256) {
        return _delegateCheckpoints[account].latest();
    }

    function getPastVotes(address account, uint256 timepoint) public view virtual override returns (uint256) {
        require(timepoint < clock(), "Votes: future lookup");
        return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint32(timepoint));
    }

    function getPastTotalSupply(uint256 timepoint) public view virtual override returns (uint256) {
        require(timepoint < clock(), "Votes: future lookup");
        return _totalCheckpoints.upperLookupRecent(SafeCast.toUint32(timepoint));
    }

    function _getTotalSupply() internal view virtual returns (uint256) {
        return _totalCheckpoints.latest();
    }

    function delegates(address account) public view virtual override returns (address) {
        return _delegation[account];
    }

    function delegate(address delegatee) public virtual override {
        address account = _msgSender();
        _delegate(account, delegatee);
    }

    function delegateBySig(
        address delegatee,
        uint256 nonce,
        uint256 expiry,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual override {
        require(block.timestamp <= expiry, "Votes: signature expired");
        address signer = ECDSA.recover(
            _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))),
            v,
            r,
            s
        );
        require(nonce == _useNonce(signer), "Votes: invalid nonce");
        _delegate(signer, delegatee);
    }

    function _delegate(address account, address delegatee) internal virtual {
        address oldDelegate = delegates(account);
        _delegation[account] = delegatee;

        emit DelegateChanged(account, oldDelegate, delegatee);
        _moveDelegateVotes(oldDelegate, delegatee, _getVotingUnits(account));
    }

    function _transferVotingUnits(address from, address to, uint256 amount) internal virtual {
        if (from == address(0)) {
            _push(_totalCheckpoints, _add, SafeCast.toUint224(amount));
        }
        if (to == address(0)) {
            _push(_totalCheckpoints, _subtract, SafeCast.toUint224(amount));
        }
        _moveDelegateVotes(delegates(from), delegates(to), amount);
    }

    function _moveDelegateVotes(address from, address to, uint256 amount) private {
        if (from != to && amount > 0) {
            if (from != address(0)) {
                (uint256 oldValue, uint256 newValue) = _push(
                    _delegateCheckpoints[from],
                    _subtract,
                    SafeCast.toUint224(amount)
                );
                emit DelegateVotesChanged(from, oldValue, newValue);
            }
            if (to != address(0)) {
                (uint256 oldValue, uint256 newValue) = _push(
                    _delegateCheckpoints[to],
                    _add,
                    SafeCast.toUint224(amount)
                );
                emit DelegateVotesChanged(to, oldValue, newValue);
            }
        }
    }

    function _push(
        Checkpoints.Trace224 storage store,
        function(uint224, uint224) view returns (uint224) op,
        uint224 delta
    ) private returns (uint224, uint224) {
        return store.push(SafeCast.toUint32(clock()), op(store.latest(), delta));
    }

    function _add(uint224 a, uint224 b) private pure returns (uint224) {
        return a + b;
    }

    function _subtract(uint224 a, uint224 b) private pure returns (uint224) {
        return a - b;
    }

    function _useNonce(address owner) internal virtual returns (uint256 current) {
        Counters.Counter storage nonce = _nonces[owner];
        current = nonce.current();
        nonce.increment();
    }

    function nonces(address owner) public view virtual returns (uint256) {
        return _nonces[owner].current();
    }

    function DOMAIN_SEPARATOR() external view returns (bytes32) {
        return _domainSeparatorV4();
    }

    function _getVotingUnits(address) internal view virtual returns (uint256);
}

_delegation

アドレスごとに委任されたアドレスを格納する配列。

_delegateCheckpoints


アカウントごとの委任された履歴を格納する配列。

_totalCheckpoints

全体の委任履歴を格納する配列。

_nonces

アドレスごとのnonceという一意の識別子を格納する配列。

clock

ある時点の基準となるポイントを返す関数。

デフォルトではブロック番号を使用して、48ビットの整数に変換して返しています。

ある基準となるポイントを取得することで、その時点で各NFTの保有者がNFTをいくつ持っているのかを確定させることができます。

CLOCK_MODE

呼び出しもとにclock関数のモードに関する情報を提供する関数。

clock関数を実行して、clock関数が正しく動作していることを確認しています。

clock関数が正常に動いている場合はmode=blocknumber&from=defaultという文字列を返します。

getVotes

特定のアドレスの現在の投票権数を返す関数。

getPastVotes

過去のある時点での特定のアドレスの投票権数を返す関数。

getPastTotalSupply

過去のある時点でのまだ投票されていない投票権の合計数を返す関数。

_getTotalSupply

現在のまだ投票されていない投票権の合計数を返す関数。

delegates

特定のアドレスが委任したアドレスの一覧を返す関数。

delegate

あるアドレスに対して投票権を委任する関数。

実行アドレスを取得しています。

delegateBySig

投票権を委任をする際の署名情報を検証しています。

以下の条件をチェックしています。

条件

  • 署名の有効期限が切れていないか?
  • 署名されたデータのハッシュ値、vrsの値を使用して署名者のアドレスの復元。
  • 署名をしたアドレスのnonceがすでに使用されていないか?

上記の条件を通過したのち委任処理を実行しています。

_delegate

特定のアドレスの現在有効な投票権を全て使用して委任する関数。

特定のアドレスの現在使用できる投票権の数を取得しています。

その後、その投票権を全て使用して引数のdelegateeで指定したアドレスに対して委任をしています。

_transferVotingUnits

投票権を移動させる関数。

fromが0アドレスの時、_totalCheckpoints_add関数を使用してamount分の投票権を追加します。

これにより総投票権が増加します。

toが0アドレスの時、_totalCheckpointsから_subtract関数を使用してamount分の投票権を破棄します。

これにより総投票権が減少します。

_moveDelegateVotes関数を呼び出して、fromで指定されたアドレスからtoに指定されたアドレスへ、amount分の投票権を移動する。

この関数を実行することにより、全体の投票権の増減や投票権の移動を管理することができるようになります。

_moveDelegateVotes

委任された投票権を移動させる関数。

まずは、fromtoが異なるアドレスであり、amountの値が0よりも大きいことを確認します。

fromが0アドレスでない場合、_delegateCheckpoints[from]という移動前のアドレスに対して_subtract関数を使用してamount分投票権を減算します。

toが0アドレスでない場合、_delegateCheckpoints[to]という移動先のアドレスに対して_add関数を使用してamount分投票権を加算します。

DelegateVotesChangedイベントを発行して、委任された投票権が移動したことをログに残します。

_push

新たなチェックポイントを作成してあるアドレスの投票権の増減などの履歴を保存する関数。

_add

2つの数値を加算して返す関数。

_subtract

2つの数値を減算して返す関数。

_useNonce

引数のownerで指定されたアドレスのnonce値を返し、+ 1する。

nonces

引数のownerで指定されたアドレスのnonce値を返す関数。

_getVotingUnits

アドレスが保有している投票権の数を返す関数。

このコントラクトを利用しているコントランクとアイで実装する必要があります。

ERC721Votes

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "../../../governance/utils/Votes.sol";

abstract contract ERC721Votes is ERC721, Votes {
    function _afterTokenTransfer(
        address from,
        address to,
        uint256 firstTokenId,
        uint256 batchSize
    ) internal virtual override {
        _transferVotingUnits(from, to, batchSize);
        super._afterTokenTransfer(from, to, firstTokenId, batchSize);
    }

    function _getVotingUnits(address account) internal view virtual override returns (uint256) {
        return balanceOf(account);
    }
}

_afterTokenTransfer

NFTが送付された時に投票権を調整する関数。

_transferVotingUnitsを実行して、ERC721規格のNFTの送付に伴う投票権の移動を実行します。

その後super._afterTokenTransferを呼び出して、親クラスの_afterTokenTransfer関数を実行します。

_getVotingUnits

特定のアドレスの投票権量を返す関数。

引数のaccountにして指定されたアドレスの投票権量(ERC721規格のNFTの数)を返します。

ERC721Royalty

NFTを販売するときのロイヤリティを強制する拡張機能。

ロイヤリティ」とは、二次流通した時の売上の一部がクリエイターにも入るというものです。

https://qiita.com/cardene/items/456ccacca080fd72e3ec#%E5%AE%9F%E8%A3%85%E4%BE%8Berc721

以下の記事でもERC721Royaltyについてまとめています。

ERC2981.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../../interfaces/IERC2981.sol";
import "../../utils/introspection/ERC165.sol";

abstract contract ERC2981 is IERC2981, ERC165 {
    struct RoyaltyInfo {
        address receiver;
        uint96 royaltyFraction;
    }

    RoyaltyInfo private _defaultRoyaltyInfo;
    mapping(uint256 => RoyaltyInfo) private _tokenRoyaltyInfo;

    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
        return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
    }

    function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual override returns (address, uint256) {
        RoyaltyInfo memory royalty = _tokenRoyaltyInfo[tokenId];

        if (royalty.receiver == address(0)) {
            royalty = _defaultRoyaltyInfo;
        }
        uint256 royaltyAmount = (salePrice * royalty.royaltyFraction) / _feeDenominator();
        return (royalty.receiver, royaltyAmount);
    }

    function _feeDenominator() internal pure virtual returns (uint96) {
        return 10000;
    }

    function _setDefaultRoyalty(address receiver, uint96 feeNumerator) internal virtual {
        require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice");
        require(receiver != address(0), "ERC2981: invalid receiver");

        _defaultRoyaltyInfo = RoyaltyInfo(receiver, feeNumerator);
    }

    function _deleteDefaultRoyalty() internal virtual {
        delete _defaultRoyaltyInfo;
    }

    function _setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) internal virtual {
        require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice");
        require(receiver != address(0), "ERC2981: Invalid parameters");

        _tokenRoyaltyInfo[tokenId] = RoyaltyInfo(receiver, feeNumerator);
    }

    function _resetTokenRoyalty(uint256 tokenId) internal virtual {
        delete _tokenRoyaltyInfo[tokenId];
    }
}

RoyaltyInfo

ロイヤリティ情報を表す構造体。

RoyaltyInfo

  • receiver
    • ロイヤリティを受け取るアドレス。
  • royaltyFraction
    • 売却価格に対するロイヤリティの割合。

_defaultRoyaltyInfo

デフォルトのロイヤリティ。

個別にロイヤリティを設定されていないNFTに使用されるデフォルトのロイヤリティ。

_tokenRoyaltyInfo

NFTごとのロイヤリティを格納している配列。

supportsInterface

IERC2981のインターフェースをサポートしているか確認する関数。

royaltyInfo

引数のtokenIdを持つNFTのロイヤリティの受け取りアドレスとロイヤリティを計算する関数。

_tokenRoyaltyInfo配列からtokenIdのロイヤリティ情報を取得します。

もし、ロイヤリティ情報が設定されていないときは、_defaultRoyaltyInfoが使用されます。

ロイヤリティ金額の計算は、売却価格に対するロイヤリティの割合(royalty.royaltyFraction)を計算して返します。

_feeDenominator

ロイヤリティ料金を計算する際の分数を解釈するための千分率を返す関数。

ロイヤリティを計算するとき、以下の式が使用されます。

売却価格 x ロイヤリティ割合(‰)/ _feeDenominator

この時、_feeDenominator部分は千分率を表しています。

千分率とは、%を10倍した数で‰(パーセンタイル)と言います。

10% = 100%
25% = 250%

なぜこのような値を使用しているかというと、「」を使用すると計算に誤差が生じる可能性があるためです。

_setDefaultRoyalty

デフォルトのロイヤリティを設定する関数。

以下の条件をチェックして処理を実行しています。

条件

  • feeNumerator_feeDenominatorより大きい値ではない。
  • receiverは0アドレスでない。

_deleteDefaultRoyalty

デフォルトのロイヤリティを削除する関数。

_setTokenRoyalty

特定のNFTにロイヤリティを設定する関数。

以下の条件が満たされている時、引数のtokenIdのNFTにロイヤリティを設定します。

条件

  • feeNumerator_feeDenominatorより大きい値ではない。
  • receiverは0アドレスでない。

_resetTokenRoyalty

特定のNFTのロイヤリティ設定を削除する関数。

ERC721Wrapper

ERC721規格のNFTをラップしたトークンを発行する拡張機能。

ERC721規格のNFTをもとに、元のERC721規格のNFTを保持しながら別のトークンに変換することを「ラップ」と言います。

NFTを「ラップ」することで、ガバナンスや投票の際にトークンを他のデジタルアセットを組み合わせたり、さらに他のトークンと相互運用性を持たせたりすることができます。

ラップ」されたトークンは元のトークンを紐付けられているため、必要応じてラップトークンを破棄して元のトークンを取り戻すことができます。

このように「ラップ」とは、トークンを別の形式に変換し、新たな機能や利用方法を提供するための手段です。

具体的な使用例としては以下が挙げられます。

使用例

  • Aというゲーム(カードゲーム)のNFTをラップして、Bというゲーム(RPG)で使用できるNFTを生成する。
  • NFTのレンタルできるように、NFTをラップしたNFTの使用権NFTを生成する。
  • NFTをラップして、複数のNFTを生成する。
    • 例)トークンID「1」のNFTをラップして、トークンID「1001」、「1002」、「1003」を生成する。

ERC721Wrapper

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../ERC721.sol";

abstract contract ERC721Wrapper is ERC721, IERC721Receiver {
    IERC721 private immutable _underlying;

    constructor(IERC721 underlyingToken) {
        _underlying = underlyingToken;
    }

    function depositFor(address account, uint256[] memory tokenIds) public virtual returns (bool) {
        uint256 length = tokenIds.length;
        for (uint256 i = 0; i < length; ++i) {
            uint256 tokenId = tokenIds[i];

            underlying().transferFrom(_msgSender(), address(this), tokenId);
            _safeMint(account, tokenId);
        }

        return true;
    }

    function withdrawTo(address account, uint256[] memory tokenIds) public virtual returns (bool) {
        uint256 length = tokenIds.length;
        for (uint256 i = 0; i < length; ++i) {
            uint256 tokenId = tokenIds[i];
            require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721Wrapper: caller is not token owner or approved");
            _burn(tokenId);
            underlying().safeTransferFrom(address(this), account, tokenId);
        }

        return true;
    }

    function onERC721Received(
        address,
        address from,
        uint256 tokenId,
        bytes memory
    ) public virtual override returns (bytes4) {
        require(address(underlying()) == _msgSender(), "ERC721Wrapper: caller is not underlying");
        _safeMint(from, tokenId);
        return IERC721Receiver.onERC721Received.selector;
    }

    function _recover(address account, uint256 tokenId) internal virtual returns (uint256) {
        require(underlying().ownerOf(tokenId) == address(this), "ERC721Wrapper: wrapper is not token owner");
        _safeMint(account, tokenId);
        return tokenId;
    }

    function underlying() public view virtual returns (IERC721) {
        return _underlying;
    }
}

_underlying

ラップされる元のERC721規格のNFTを保持するプライベートかつ、変更不能な値です。

depositFor

ERC721規格のNFTを預け入れて、それに対応するトークンを発行する関数。

tokenIdsの配列の要素数分以下の処理を行います。

処理

  • NFTの所有者アドレスから、コントラクトアドレスにNFTを送付します。
  • _safeMintを実行して、accountに指定されたアドレスに預け入れたNFTのトークンIDに対応するトークンを発行する。

全ての処理が完了したのち、trueを呼び出しもとに返します。

withdrawTo

ラップされたトークンを破棄して、対応するNFTを取り戻す関数。

tokenIdsの配列の要素数分以下の処理を行います。

処理

  • _isApprovedOrOwner関数を実行して、関数実行アドレスがトークンIDの一致するNFTの所有者、もしくは所有者から権限を与えられたアドレスか確認する。
  • _burn関数を実行して、ラップしたトークンを破棄する。
  • underlying関数を実行して、破棄したトークンに対応する元のNFTをコントラクトから引数で指定されたaccountのアドレスに送付する。

全ての処理が完了したのち、trueを呼び出しもとに返します。

onERC721Received

コントラクトにERC721規格のNFTが送られてきた時に、ラップされたトークンを発行する関数。

underlying関数を実行して、ERC721規格のNFTを発行しているコントラクトアドレスと、関数を実行したアドレスが一致するか確認します。

もし一致しない場合はエラーを返します。

_safeMint関数を実行して、指定されたトークンIDのトークンを発行します。

これによりラップされたトークンが生成され、元のNFTの所有者のアドレスへトークンが付与されます。

IERC721Receiver.onERC721Received.selectorを呼び出しもとに返して、送付元のコントラクトが正常に送付処理を完了したことを伝えます。

_recover

間違ってERC721規格のNFTをこのコントラクトに送付してしまった場合に、対応するトークンIDのラップされたトークンを発行する関数。

underlying().ownerOf(tokenId)を使用して、引数で指定されたtokenIdの所有者がこのコントラクトであることを確認します。

もし所有者がこのコントラクトではない場合、エラーを返します。

_safeMint関数を実行して、引数で指定されたaccountのアドレスに同じトークンIDのラップされたトークンを発行します。

これにより、対応する元のNFTを間違って送付した場合でも、同じトークンIDのラップされたトークンを生成できます。

tokenIdを呼び出しもとに返します。

underlying

ラップしている元のERC721規格のNFTコントラクトを返す関数。

最後に

今回はERC721の拡張機能を1つずつ紹介してきました。

全部で7つもあったので結構長くなってしまいましたがいかがだったでしょうか?

ポイント

  • ERC721の拡張機能についてざっくり理解できた!
  • ERC721の拡張機能にどんなものがあるのか知らなかったから参考になった!
  • ERC721の拡張機能がそれぞれどんなことをしているの知らなかったから勉強になった!

全部読み終わってこのような状態でいてくれたら嬉しいです!

もし何か質問などがあれば以下のTwitterなどから連絡ください!

普段はSolidityやブロックチェーン、Web3についての情報発信をしています。

Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!

-Openzeppelin, Smart Contract, Solidity
-, ,