「Momoguro: Holoself」のソースコードを見てみた
string greeting = "こんにちは、web3エンジニアのポコ太郎です。";
今回の記事では、2023年3月2日にブロックチェーン上に作成された「Momoguro: Holoself」のスマートコントラクトについて紹介していきます。
Baobab Studios とは
Baobab Studiosは、アメリカのカリフォルニア州に本拠を置く、VRおよびインタラクティブエンターテインメントのためのクリエイティブスタジオです。同社は、映画のようなストーリーテリングと、新しいテクノロジーを組み合わせた、インタラクティブなVRエクスペリエンスを提供しています。
Momoguro とは
Momoguroは、Baobab Studiosによって立ち上げられたデジタルコレクション・ストーリーテリング・ワールドです。
ワールドには「Momo」と呼ばれる生き物が存在し、融合することで強力な力を発揮することができます。
Momoguroの最初のリリースは、デジタル収集可能なストーリーテリングRPGです。このRPGでは、プレイヤーたちは「Holoself」という存在になり、モモを収集し、クエストに挑戦し、「Uno Plane」という世界の物語を発見することができます。
Momoguro:
https://momoguro.com/
OpenSea Momoguro: Holoself:
https://opensea.io/ja/collection/momoguro-holoself
Etherscan:
https://etherscan.io/address/0x59ad67e9c6a84e602bc73b3a606f731cc6df210d
スマートコントラクト
Holoself.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
Solidityのバージョンは2022年9月8日にリリースされた 0.8.17。
ちなみに、現時点での最新版は2023年2月22日にリリースされた 0.8.19です。
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {ERC721AQueryable} from "erc721a/contracts/extensions/ERC721AQueryable.sol";
import {IERC721A, ERC721A} from "erc721a/contracts/ERC721A.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import {IERC2981, ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol";
import {OperatorFilterer} from "../OperatorFilterer.sol";
ERC721AQueryable
クエリ機能を備えたERC721Aサブクラスです。
tokensOfOwner関数を実行すると指定したアドレスが保有しているトークンIDの一覧が取得できます。
ERC2981
NFT(Non-Fungible Token)のロイヤリティ権をサポートするために設計されたコントラクトです。
NFTの初回販売時に販売者が決定したロイヤルティ率が保持され、その後のセカンダリーマーケットでの販売時にも自動的に受け取ることができます。
OperatorFilterer
クリエイターへの報酬を強制するためのフィルターです。
クリエイターへ報酬を還元しないアドレス(OpenSeaがブラックリストしているマーケットプレイスなどのコントラクトアドレスなど)は、ApproveやTransferFromができないようになります。
TwitFiなど、最近実装しているNFTが増えてきました。
// Supply Error
error ExceedsMaxSupply();
// Sale Errors
error SaleNotActive();
error Unauthorized();
// Limit Errors
error TxnLimitReached();
error MintLimitReached();
// Utility Errors
error TimeCannotBeZero();
// Withdrawl Errors
error ETHTransferFailDev();
error ETHTransferFailOwner();
// General Errors
error AddressCannotBeZero();
error CallerIsAContract();
error IncorrectETHSent();
エラーは全て「revert」で実装されていました。
/// @title Momoguro Holoself NFT Contract
/// @notice This is the primary Momoguro Holoself NFT contract.
/// @notice This contract implements marketplace operator filtering
/// @dev This contract is used to mint Assets for the Momoguro project.
contract Holoself is
ERC721AQueryable,
Ownable,
OperatorFilterer,
ERC2981,
ReentrancyGuard
NatSpecを使用する場合、コメントアウトはスラッシュ3つ。
address payable public developerFund;
address payable public ownerFund;
bytes32 private _presaleMerkleRoot;
uint256 public constant MAX_SUPPLY = 8888;
uint256 public constant MINT_LIMIT_PER_ADDRESS = 2;
uint256 public MINT_PRICE = 0.22 ether;
bool public operatorFilteringEnabled;
bool public publicSaleActive = false;
bool public preSaleActive = false;
string private _baseTokenURI;
mapping(address => uint256) public userMinted;
状態変数の一覧。
event UpdateBaseURI(string baseURI);
event UpdateSalePrice(uint256 _price);
event UpdatePresaleStatus(bool _preSale);
event UpdateSaleStatus(bool _publicSale);
event UpdatePresaleMerkleRoot(bytes32 merkleRoot);
イベントの一覧。
constructor(address _developerFund, address _ownerFund)
ERC721A("Holoself", "Holo")
{
if (
address(_developerFund) == address(0) ||
address(_ownerFund) == address(0)
) revert AddressCannotBeZero();
// Set withdrawl addresses.
developerFund = payable(_developerFund);
ownerFund = payable(_ownerFund);
// Register for operator filtering, disable by default.
_registerForOperatorFiltering();
operatorFilteringEnabled = true;
// Set default royalty to 5% (denominator out of 10000).
_setDefaultRoyalty(0xeA803944E87142d44b945b3f5a0639f442ba361B, 500);
}
ブロックチェーン上にコントラクトが作成されるタイミングで、一度だけ実行される関数。
FundAddressやOperatorFiltererの初期設定がされています。
//===============================================================
// Modifiers
//===============================================================
modifier callerIsUser() {
if (tx.origin != msg.sender) revert CallerIsAContract();
_;
}
modifier requireCorrectEth(uint256 _quantity) {
if (msg.value != MINT_PRICE * _quantity) revert IncorrectETHSent();
_;
}
callerIsUser
外部のEOAから呼び出された場合にrevert
悪意のあるコントラクトが別のコントラクトを呼び出し、外部アカウントになりすましてアクセス制御を回避しようとするなど、特定のタイプの攻撃を防ぐために使用することができます。
requireCorrectEth
指定されたETH価格と異なる価格でトランザクションが送信された場合にrevert
//===============================================================
// Minting Functions
//===============================================================
/**
** @dev The mint function requires the user to send the exact amount of ETH
** required for the transaction to eliminate the need for returning overages.
** @param _quantity The quantity to mint
*/
function mint(uint256 quantity)
external
payable
callerIsUser
nonReentrant
requireCorrectEth(quantity)
{
if (!publicSaleActive) revert SaleNotActive();
if (totalSupply() + quantity > MAX_SUPPLY) revert ExceedsMaxSupply();
if (userMinted[msg.sender] + quantity > MINT_LIMIT_PER_ADDRESS)
revert MintLimitReached();
userMinted[msg.sender] += quantity;
_mint(msg.sender, quantity);
}
ミント関数。
/**
** @notice The presaleMint function only accepts a single transaction per wallet.
** it also expects a byte32 slice as calldata to provide valid proof of list.
** @dev The presaleMint function requires the user to send the exact amount of ETH
** required for the transaction to eliminate the need for returning overages.
** @param _merkleProof The merkle proof in byte32[] format
** @param _quantity The quantity to mint
*/
function presaleMint(bytes32[] calldata _merkleProof, uint256 quantity)
external
payable
callerIsUser
nonReentrant
requireCorrectEth(quantity)
{
if (!preSaleActive) revert SaleNotActive();
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
if (!MerkleProof.verifyCalldata(_merkleProof, _presaleMerkleRoot, leaf))
revert Unauthorized();
if (totalSupply() + quantity > MAX_SUPPLY) revert ExceedsMaxSupply();
if (_getAux(msg.sender) != 0) revert TxnLimitReached();
if (userMinted[msg.sender] + quantity > MINT_LIMIT_PER_ADDRESS)
revert MintLimitReached();
userMinted[msg.sender] += quantity;
_setAux(msg.sender, 1);
_mint(msg.sender, quantity);
}
プレセール時のミント関数。
_presaleMerkleRootで、対象者をバリデート。
//
function devMint(address _to, uint256 quantity) external payable onlyOwner {
if (totalSupply() + quantity > MAX_SUPPLY) revert ExceedsMaxSupply();
_mint(_to, quantity);
}
0.22 ETHがなくてもミントできるオーナー用のミント関数。
function _baseURI() internal view virtual override returns (string memory) {
return _baseTokenURI;
}
ベースURLを返してくれる関数。
//===============================================================
// Setter Functions
//===============================================================
function setBaseURI(string calldata baseURI) external onlyOwner {
_baseTokenURI = baseURI;
emit UpdateBaseURI(baseURI);
}
function setSalePrice(uint256 _price) external onlyOwner {
MINT_PRICE = _price;
emit UpdateSalePrice(_price);
}
function setPresaleStatus(bool _preSale) external onlyOwner {
preSaleActive = _preSale;
emit UpdatePresaleStatus(_preSale);
}
function setSaleStatus(bool _publicSale) external onlyOwner {
publicSaleActive = _publicSale;
emit UpdateSaleStatus(_publicSale);
}
function setPresaleMerkleRoot(bytes32 merkleRoot) external onlyOwner {
_presaleMerkleRoot = merkleRoot;
emit UpdatePresaleMerkleRoot(merkleRoot);
}
セッター関数。
ミントの価格は変更できるようになっている。
//===============================================================
// ETH Withdrawl
//===============================================================
function withdraw() external onlyOwner nonReentrant {
uint256 currentBalance = address(this).balance;
uint256 amount1 = (currentBalance * 5.5e19) / 1e21;
uint256 amount2 = currentBalance - amount1;
(bool success1, ) = payable(developerFund).call{value: amount1}("");
if (!success1) revert ETHTransferFailDev();
(bool success2, ) = payable(ownerFund).call{value: amount2}("");
if (!success2) revert ETHTransferFailOwner();
}
コントラクトに保有されている残高を出金する関数。
5.5%をdeveloperFundに出金し、残りをownerFundに出金しています。
amount1 = (currentBalance * 5.5e19) / 1e21
= (currentBalance * 5.5 * 10^19) / (1 * 10^21)
= (currentBalance * 5.5) / 100
= (currentBalance * 0.055)
計算は上記のように表すことができます。
//===============================================================
// Operator Filtering
//===============================================================
function setApprovalForAll(address operator, bool approved)
public
override(IERC721A, ERC721A)
onlyAllowedOperatorApproval(operator)
{
super.setApprovalForAll(operator, approved);
}
function approve(address operator, uint256 tokenId)
public
payable
override(IERC721A, ERC721A)
onlyAllowedOperatorApproval(operator)
{
super.approve(operator, tokenId);
}
function transferFrom(
address from,
address to,
uint256 tokenId
) public payable override(IERC721A, ERC721A) onlyAllowedOperator(from) {
super.transferFrom(from, to, tokenId);
}
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) public payable override(IERC721A, ERC721A) onlyAllowedOperator(from) {
super.safeTransferFrom(from, to, tokenId);
}
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public payable override(IERC721A, ERC721A) onlyAllowedOperator(from) {
super.safeTransferFrom(from, to, tokenId, data);
}
function setOperatorFilteringEnabled(bool value) external onlyOwner {
operatorFilteringEnabled = value;
}
function _operatorFilteringEnabled() internal view override returns (bool) {
return operatorFilteringEnabled;
}
function _isPriorityOperator(address operator)
internal
pure
override
returns (bool)
{
// OpenSea Seaport Conduit:
// https://etherscan.io/address/0x1E0049783F008A0085193E00003D00cd54003c71
// https://goerli.etherscan.io/address/0x1E0049783F008A0085193E00003D00cd54003c71
return operator == address(0x1E0049783F008A0085193E00003D00cd54003c71);
}
OperatorFiltererを使用する場合は、このような感じでオーバーライドが必要。
//===============================================================
// ERC2981 Implementation
//===============================================================
function setDefaultRoyalty(address receiver, uint96 feeNumerator)
external
onlyOwner
{
_setDefaultRoyalty(receiver, feeNumerator);
}
ロイヤリティをセットする関数です。
//===============================================================
// SupportsInterface
//===============================================================
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(IERC721A, ERC721A, ERC2981)
returns (bool)
{
// Supports the following `interfaceId`s:
// - IERC165: 0x01ffc9a7
// - IERC721: 0x80ac58cd
// - IERC721Metadata: 0x5b5e139f
// - IERC2981: 0x2a55205a
return
ERC721A.supportsInterface(interfaceId) ||
ERC2981.supportsInterface(interfaceId);
}
サポートしているインターフェースを返す関数。
function _startTokenId() internal view virtual override returns (uint256) {
return 1;
}
一番初めのトークンIDを返します。
デフォルト値は「0」なので、「1」に書き換えるためにERC721Aの関数をオーバーライド。
TokenID [1] の送り先
マルチシグを導入している可能性があり、onlyOwnerが設定されている関数は複数のアカウントからの署名がないと実行できないように制御されています。
トランザクションhttps://etherscan.io/tx/0xe01516990cd49b587c09751c5e11eff467c4472c55be009b75e81892f3856b45
送り先アドレス「GnosisSafeProxy」
https://etherscan.io/token/0x59ad67e9c6a84e602bc73b3a606f731cc6df210d?a=0xc1397db9491cf17fc2b8ae4119a9d6c6f715a163
GnosisSafeProxy
GnosisSafeProxyは、Gnosis Safeと呼ばれる多数の署名者によるウォレットの実装に使用されるスマートコントラクトです。Gnosis Safeは、暗号通貨やトークンなどのデジタル資産を保管するための安全なウォレットです。複数の署名者が必要なマルチシグウォレットを実現するために使用されることが一般的です。
ENS「momogurovault.eth」
ENSを取得しているトランザクションを見つけたので、今後、新しいサービスが出てくる可能性がありそうですね。
トランザクション
https://etherscan.io/tx/0x37c55ea4df015a07ed3baa15f13a1c1f49fef50b87e4f8eaa5c7ccdcdd90e592
All That Node
DApp開発や運用でオススメのノードです。
All That Nodeは、DSRVを利用したマルチチェーンノードプラットフォームです。このサービスの大きな特徴は、Aptosなど20以上の主要なプロトコルを採用している点で、これからもサポートするチェーンは拡大していく予定です。
株式会社RuckPlus
株式会社RuckPlusは、現在web3エンジニアに興味がある方や目指している方を募集しています。私自身が、直接web3開発についてレクチャーをし、今後のエンジニアとしてのキャリアパスを一緒に計画していく環境を提供いたします。
DApp開発の受託開発も承っておりますので、是非、ご連絡をお待ちしております!
最後までお読みいただき、ありがとうございました。