こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
CryptoGamesは、「クリプトスペルズ」をはじめとするブロックチェーンゲームの開発・運営や、ブロックチェーン関連の開発を行っている会社です。
現在エンジニアを中心に積極採用中ですので、少しでも興味を持った方はぜひお話しだけでもできたら嬉しいです!
以下のWantedlyのサイトで会社について詳しく知ることができます!
https://www.wantedly.com/companies/cryptogames
以下のストーリーでは、実際にCryptoGamesで働いているメンバーのリアルな声を知ることができます。
https://www.wantedly.com/companies/cryptogames/stories
ぜひ上記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());
}
}
contracts/security/Pausable.sol
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");
}
}
contracts/token/ERC721/extensions/ERC721Pausable.sol
_beforeTokenTransfer
この関数はERC721コントラクトのNFTの作成や送付機能を実行する前に呼ばれる関数です。
そのため、処理を実行する前に何かしらチェックするのに役立ちます。
先ほどのPausable.sol
をインポートして使用しています。
ERC721Burnable
特定のNFTを破棄する拡張機能。
既に発行済みのERC721規格のNFTを破棄する関数です。
この機能は拡張機能ですが、OpenzeppelinではERC721.sol
で実装されています。
contracts/token/ERC721/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);
}
}
contracts/token/ERC721/extensions/ERC721Burnable.sol
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);
}
...
contracts/token/ERC721/ERC721.sol
_burn
破棄したいERC721規格のNFTのトークンIDを使用して、所有者のアドレスを取得しています。
トークンIDとは、NFTを識別するユニークな値です。「1」や「2」など数字が使われることが多いです。
その後、_beforeTokenTransfer
関数を呼びだし、何かしら処理が書かれていたらそれを実行します。
なんらかの処理で所有者が変わっている可能性もあるため、再度破棄したいNFTの所有者のアドレスを取得しています。
破棄したいNFTの操作権限を与えられているアドレスの許可を外します。
破棄したいNFTの所有者の保有NFT量を-1
して、所有しているという記録も削除します。
最後にTransfer
イベントを発行して、_afterTokenTransfer
関数を実行して何かしらの処理が書かれていたらそれを実行します。
ERC721Consecutive
一度に複数のERC721規格のNFTを発行する拡張機能。
ERC721規格のNFTをコントラクトのデプロイ時のみ一度に複数発行することができる拡張機能です。
ERC2309での提案をもとに作成されています。
ERC2309については以下を参考にしてください。
[ERC2309] 複数のNFTの作成や送付時に発行されるログの標準規格を理解しよう!
注意点としては、この拡張機能をが実行されているとき、単一の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;
}
}
contracts/token/ERC721/extensions/ERC721Consecutive.sol
_maxBatchSize
一度に発行できるERC721規格のNFTの最大数を取得する関数。
NFTを発行するときに最大値を設定していないと、やたら多くの枚数を発行される可能性があります。
その影響で本来NFTを受け取れたはずのアドレスが受け取れなかったり、トランザクションが通らなくなったりなどの問題が起こります。
_ownerOf
特定のERC721規格のNFTの所有者を返す関数。
所有者が0アドレス、もしくは引数で渡しているtokenId
がuint96の範囲内であれば、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
)が返されます。
exists
がtrue
の場合、最新のトークンIDlatestId
に1
を加えた発行されたNFTの総量である値を返します。
exists
がfalse
の場合、発行量として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];
}
}
}
contracts/token/ERC721/extensions/ERC721URIStorage.sol
_tokenURIs
それぞれのERC721規格のNFTのURIを紐づけている配列。
supportsInterface
特定のコントラクトがERC4906
をサポートしているかチェックする関数。
interfaceId
という引数と0x49064906
という値が等しいかどうか確認しています。
等しい場合はtrue
を返してERC4906
をサポートしていることを表し、異なる場合は親クラスであるIERC165
のsupportsInterface
を呼び出します。
tokenURI
特定のERC721規格のNFTのトークンURIを取得する関数。
_requireMinted
を実行して、引数で渡されたtokenId
が存在するか確認します。
存在すれば、tokneId
のトークンURIと_baseURI
関数を実行してベースURIを取得します。
もしベースURIが設定されていなければ、トークンURIを呼び出しもとに返します。
ベースURIとトークンURIが両方を連結したURIを呼び出しもとに返します。
ベースURIとトークンURIの両方とも存在しないときは、親クラスであるsuper.tokenURI
関数を呼び出します。
_setTokenURI
特定のERC721規格のNFTのトークンURIを設定・変更する関数。
引数で渡されたtokenId
が存在するか確認します。
存在すれば、引数で渡された_tokenURI
をtokenId
の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);
}
contracts/governance/utils/Votes.sol
_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
投票権を委任をする際の署名情報を検証しています。
以下の条件をチェックしています。
条件
- 署名の有効期限が切れていないか?
- 署名されたデータのハッシュ値、
v
、r
、s
の値を使用して署名者のアドレスの復元。 - 署名をしたアドレスの
nonce
がすでに使用されていないか?
上記の条件を通過したのち委任処理を実行しています。
_delegate
特定のアドレスの現在有効な投票権を全て使用して委任する関数。
特定のアドレスの現在使用できる投票権の数を取得しています。
その後、その投票権を全て使用して引数のdelegatee
で指定したアドレスに対して委任をしています。
_transferVotingUnits
投票権を移動させる関数。
from
が0アドレスの時、_totalCheckpoints
に_add
関数を使用してamount
分の投票権を追加します。
これにより総投票権が増加します。
to
が0アドレスの時、_totalCheckpoints
から_subtract
関数を使用してamount
分の投票権を破棄します。
これにより総投票権が減少します。
_moveDelegateVotes
関数を呼び出して、from
で指定されたアドレスからto
に指定されたアドレスへ、amount
分の投票権を移動する。
この関数を実行することにより、全体の投票権の増減や投票権の移動を管理することができるようになります。
_moveDelegateVotes
委任された投票権を移動させる関数。
まずは、from
とto
が異なるアドレスであり、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);
}
}
contracts/token/ERC721/extensions/ERC721Votes.sol
_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についてまとめています。
NFTのロイヤリティ強制化!? ERC2981について理解しよう!
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];
}
}
contracts/token/common/ERC2981.sol
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
」を生成する。
- 例)トークンID「
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;
}
}
contracts/token/ERC721/extensions/ERC721Wrapper.sol
_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では気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!