2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TDU_データ科学・機械学習研究室Advent Calendar 2022

Day 14

marimoNFTのコントラクトを読んでみる

Last updated at Posted at 2022-12-13

はじめに

アドベントカレンダー 日目
機械学習系のネタが思いつかなかったので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);
  }

  ...
}

おわりに

変化ルールをオンチェーンで管理していくのは面白いと感じた,なにか作ってみたい.

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?