はじめに
アドベントカレンダー 日目
機械学習系のネタが思いつかなかったのでNFTのコントラクトを読んでみる
一か月前くらいに初めてイーサリアムという概念を知った人間なので,間違いがあればコメントを頂けると助かります.
NFTとは
非代替性トークン,中には数億円で取引されているトークンもある.
イーサリアム上では,ERC721(又は,ERCERC1155)の規格で実装されていることが多い.
なぜ marimo NFTなのか
一度,何かのコントラクトを読んでみたかった.
日本のプロジェクトなので,とっつきやすかった + 変化していくNFTみたいなものに興味があった.
( marimo NFT定期的に水を取り替えることで,育っていくNFT )
参考
読んでみる
etherscanより
コントラクト
Ownable
: コントラクトのオーナ管理
ERC721AQueryable
: Azukiが開発したERC721互換の規格・複数の同時mintの際にgas代が節約できるらしい
contract Marimo is Ownable, ERC721AQueryable {
...
}
参考
変数・型
()括弧内は執筆時点での値
contract Marimo is Ownable, ERC721AQueryable {
...
string baseURI;
uint256 public immutable maxPerAddressDuringMint; // 一人がmintできる上限数 (10)
uint256 public immutable collectionSize; // コレクションのサイズ (10000)
mapping(uint256 => uint256) private _generatedAt; // 生成された日付
mapping(uint256 => uint256) private _lastWaterChangedAt; // 最後に水をあげた日
mapping(uint256 => uint16) private _lastSize; // 最新のマリモのサイズ
uint256 private constant _MINT_PRICE = 0.01 ether; // minするときの価格
uint256 private _historyIndex; // 特定のトークンの履歴の終端index
struct Stats { // 各トークンの設定値の為の型
uint8 power;
uint8 speed;
uint8 stamina;
uint8 luck;
}
struct ChangeWaterHistory { // 水を変えた際のログの為の型
address changer; // 水換えを行ったアドレス
uint256 changedAt; // 日付け
}
event ChangedStats( // 設定の変更が行われた時のイベント
uint256 indexed _tokenId
);
event ChangedWater( // 水換えが行われた時のイベント
uint256 indexed _tokenId,
address indexed _changer,
uint256 _historyIndex,
uint256 _changedAt
);
mapping(uint256 => Stats) public tokenStats; // token id => stats // 各トークンの設定参照
mapping(uint256 => uint256[]) public tokenHistoryIndexes; // token id => historyIndexes // 各トークンの履歴参照
ChangeWaterHistory[] public changeWaterHistories; // 各トークンの水替え履歴参照
bytes32 public merkleRoot; // preMint権限の為のマークル木のルートノード
bool public publicSale; // mint許可 (true)
bool public preSale; // preSaleの許可 (true)
bool public endOfSale; // mintの終了 (false)
...
}
コンストラクタ
アドレス当たりの発行可能数と,コレクション全体のサイズを渡す
contract Marimo is Ownable, ERC721AQueryable {
...
constructor(uint256 maxBatchSize_, uint256 collectionSize_) ERC721A("Marimo", "MRM") {
maxPerAddressDuringMint = maxBatchSize_;
collectionSize = collectionSize_;
}
...
}
Getter
contract Marimo is Ownable, ERC721AQueryable {
...
function getAge(uint256 tokenId) external view returns (uint256) { // 生成からの経過時間
require(_exists(tokenId), "no token"); // トークンの存在確認
return block.timestamp - _generatedAt[tokenId];
}
function getElapsedTimeFromLastWaterChanged(uint256 tokenId) public view returns (uint256) { // 最後に水をあげた時点からの経過時間
require(_exists(tokenId), "no token");
return block.timestamp - _lastWaterChangedAt[tokenId];
}
function getLastSize(uint256 tokenId) internal view returns (uint16) { // 最後に水をあげた時のサイズ
require(_exists(tokenId), "no token");
return _lastSize[tokenId] > 0 ? _lastSize[tokenId] : 250; // 最後のサイズが0以下であれば,250(初期値?)を返す
}
...
}
現在のサイズを取得するGetter
contract Marimo is Ownable, ERC721AQueryable {
...
function getCurrentSize(uint256 tokenId) public view returns (uint16) {
require(_exists(tokenId), "no token");
uint256 elapsedTime = getElapsedTimeFromLastWaterChanged(tokenId);
uint256 coefficient = 90000 minutes; // 1mm成長するのにかかる時間
// add constacont(1440, 33840, 79920, 94320) as the initial value when elapsedTime is zero in each cases
if (elapsedTime <= 20 days) {
return uint16((100 * elapsedTime) / coefficient + getLastSize(tokenId));
} else if (elapsedTime <= 50 days) { // 100%だった時の差分(1440 * 60 = 60 * 60 * 24 * 20 * 0.05)を調整して返す
return uint16((95 * elapsedTime + 1440 * 60 * 100) / coefficient + getLastSize(tokenId));
} else if (elapsedTime <= 80 days) {
return uint16((50 * elapsedTime + 33840 * 60 * 100) / coefficient + getLastSize(tokenId));
} else if (elapsedTime <= 100 days) {
return uint16((10 * elapsedTime + 79920 * 60 * 100) / coefficient + getLastSize(tokenId));
} else {
return uint16(getLastSize(tokenId) + (94320 * 60 * 100 / coefficient));
}
}
...
}
水替え
contract Marimo is Ownable, ERC721AQueryable {
...
function changeWater(uint256 tokenId) external {
require(_exists(tokenId), "no token");
require(_lastSize[tokenId] == 0 || block.timestamp - _lastWaterChangedAt[tokenId] > 1 days, "only once a day"); // 水替えは一日一回のみ
_lastSize[tokenId] = getCurrentSize(tokenId); // update lastSize before update lastWaterChangedAt // 現在のサイズ取得
_lastWaterChangedAt[tokenId] = block.timestamp; // 水替え日更新
changeWaterHistories.push(ChangeWaterHistory(msg.sender, block.timestamp)); // 履歴登録
tokenHistoryIndexes[tokenId].push(_historyIndex); // 履歴を登録
emit ChangedWater(tokenId, msg.sender, _historyIndex, block.timestamp); // イベント発火
_historyIndex += 1; // index更新
}
...
}
Mint
contract Marimo is Ownable, ERC721AQueryable {
...
function publicMint(uint256 quantity) payable external returns (uint256) {
require(publicSale, "inactive"); // 販売許可
require(!endOfSale, "end of sale"); // 販売終了
require(totalSupply() + quantity <= collectionSize, "reached max supply"); // 売り切れ
require(_numberMinted(msg.sender) + quantity <= maxPerAddressDuringMint, "wrong num"); // アドレスあたりの制限
require(msg.value == _MINT_PRICE * quantity, "wrong price"); // 支払い確認
uint256 nextTokenId = _nextTokenId();
for (uint256 i = nextTokenId; i < nextTokenId + quantity; i++) { // 各変数の初期化
_generatedAt[i] = block.timestamp; // 発行日
_lastWaterChangedAt[i] = block.timestamp; // 水替え日
tokenStats[i] = _computeStats(i); // 設定
emit ChangedStats(i); // イベント発火
}
_mint(msg.sender, quantity); // mint
return nextTokenId; // idを返す
}
function isWhiteListed(bytes32[] calldata _merkleProof) public view returns(bool) { // ホワイトリスト管理
bytes32 leaf = keccak256(abi.encodePacked(msg.sender)); // 送信者のアドレスをハッシュ化
return MerkleProof.verify(_merkleProof, merkleRoot, leaf); // マールク木の葉であるかどうかを判定
}
function numberMinted() external view returns (uint256) {
return _numberMinted(msg.sender);
}
function preMint(uint256 quantity, bytes32[] calldata _merkleProof) payable external returns(uint256) {
require(preSale, "inactive");
require(!endOfSale, "end of sale");
require(totalSupply() + quantity <= collectionSize, "reached max supply");
require(_numberMinted(msg.sender) + quantity <= maxPerAddressDuringMint, "wrong num");
require(msg.value == _MINT_PRICE * quantity, "wrong price");
require(isWhiteListed(_merkleProof), "invalid proof"); // 権限をもっているかどうか
uint256 nextTokenId = _nextTokenId();
for (uint256 i = nextTokenId; i < nextTokenId + quantity; i++) {
_generatedAt[i] = block.timestamp;
_lastWaterChangedAt[i] = block.timestamp;
tokenStats[i] = _computeStats(i);
emit ChangedStats(i);
}
_mint(msg.sender, quantity);
return nextTokenId;
}
...
}
参考
Setterなど
contract Marimo is Ownable, ERC721AQueryable {
...
function _startTokenId() internal pure override returns (uint256) {
return 1;
}
function withdraw() external onlyOwner { // 収益の引き出し
payable(owner()).transfer(address(this).balance);
}
function _baseURI() internal view virtual override returns (string memory) {
return baseURI;
}
function setBaseURI(string memory _newBaseURI) external onlyOwner {
baseURI = _newBaseURI;
}
function setPreSale(bool _preSale) external onlyOwner {
preSale = _preSale;
}
function setPublicSale(bool _publicSale) external onlyOwner {
publicSale = _publicSale;
}
function setEndOfSale(bool _endOfSale) external onlyOwner {
endOfSale = _endOfSale;
}
function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner {
merkleRoot = _merkleRoot;
}
...
}
その他
contract Marimo is Ownable, ERC721AQueryable {
...
function _computeStats(uint256 tokenId) internal view returns (Stats memory) { // 設定値の計算
uint256 pseudorandomness = uint256( // 一つ前のブロックのハッシュ値をトークンIDと連結して再度ハッシュ化
keccak256(abi.encodePacked(blockhash(block.number - 1), tokenId))
);
uint8 power = uint8(pseudorandomness) % 10 + 1; //
uint8 speed = uint8(pseudorandomness >> 8 * 1) % 10 + 1; // 8bitシフトして 1~10
uint8 stamina = uint8(pseudorandomness >> 8 * 2) % 10 + 1; // 16bit
uint8 luck = uint8(pseudorandomness >> 8 * 3) % 10 + 1;
return Stats(power, speed, stamina, luck);
}
...
}
おわりに
変化ルールをオンチェーンで管理していくのは面白いと感じた,なにか作ってみたい.