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

# はじめに

アドベントカレンダー 日目
機械学習系のネタが思いつかなかったのでNFTのコントラクトを読んでみる
一か月前くらいに初めてイーサリアムという概念を知った人間なので，間違いがあればコメントを頂けると助かります．

# NFTとは

非代替性トークン，中には数億円で取引されているトークンもある．
イーサリアム上では，ERC721(又は，ERCERC1155)の規格で実装されていることが多い．

# なぜ marimo NFTなのか

一度，何かのコントラクトを読んでみたかった．
日本のプロジェクトなので，とっつきやすかった + 変化していくNFTみたいなものに興味があった．

( marimo NFT定期的に水を取り替えることで，育っていくNFT )

### 参考

https://marimo.life/ja

https://etherscan.io/address/0xa35aa193f94a90eca0ae2a3fb5616e53c1f35193

https://opensea.io/collection/marimo-life

# 読んでみる

etherscanより

## コントラクト

`Ownable`: コントラクトのオーナ管理
`ERC721AQueryable`: Azukiが開発したERC721互換の規格・複数の同時mintの際にgas代が節約できるらしい

```solidity
contract Marimo is Ownable, ERC721AQueryable {
    ...
}
```

### 参考

https://github.com/chiru-labs/ERC721A

https://mirror.xyz/rmanzoku.eth/vw5Qg6caMYIw5HljtVwAfDU5kXJmZ76ZosO4cfiyMHE


## 変数・型

()括弧内は執筆時点での値

```solidity
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)

  ...
}
```

## コンストラクタ

アドレス当たりの発行可能数と，コレクション全体のサイズを渡す

```solidity
contract Marimo is Ownable, ERC721AQueryable {
  ...

  constructor(uint256 maxBatchSize_, uint256 collectionSize_) ERC721A("Marimo", "MRM") {
    maxPerAddressDuringMint = maxBatchSize_;
    collectionSize = collectionSize_;
  }

  ...
}
```

## Getter

```solidity
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

```solidity
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));
    }
  }

  ...
}
```

## 水替え

```solidity
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

```solidity
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;
  }

  ...
}
```

### 参考

https://gaiax-blockchain.com/merkle-tree

## Setterなど

```solidity
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;
  }

  ...
}
```

## その他

```solidity
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);
  }

  ...
}
```

# おわりに

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