# [ACP99] コントラクトでL1バリデータを管理する仕組みを理解しよう！

> ACP99Managerは、Avalanche上の任意のEVMチェーンにデプロイできるValidatorManager（バリデータを管理するコントラクト）の標準仕様を定める提案です。

- URL: https://cardene.dev/articles/61546e03f0abe6df657b
- 公開日: 2025-09-20
- 更新日: 2025-09-20
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

https://amzn.asia/d/gxvJ0Pw

今回は、AvalancheのEVMチェーンでL1のバリデータ集合（追加・削除・重み変更）を管理するためのコントラクトを提案している**ACP99**についてまとめていきます！

:::message
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
:::

https://github.com/avalanche-foundation/ACPs/blob/main/ACPs/99-validatorsetmanager-contract/README.md

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

`ACP99Manager`は、Avalanche上の任意のEVMチェーンにデプロイできるValidatorManager（バリデータを管理するコントラクト）の標準仕様を定める提案です。
これは**ACP-77 (Reinventing Subnets)**で示された仕組みに依存しており、その仕様が「**実装可能（Implementable）**」とされることが前提になります。

:::message
**ACP77**については以下の記事を参考にしてください。
:::

[\[ACP77\] バリデータ管理をL1に移す仕組みを理解しよう！](/articles/3c28cdfe275253b75da5)

**ACP77**では、P-Chainに保存されているL1のValidatorSetをAvalancheネットワーク内の任意のチェーンから管理できる提案がされています。
Subnetを`ConvertSubnetToL1Tx`によってL1に変換するとき、`(blockchainID, address)`の組み合わせで「`ValidatorManager`」を指定でき、そのアドレスが`RegisterL1ValidatorTx`や`SetL1ValidatorWeightTx`といったICMメッセージをP-Chainへ送る役割を担います。

また、ValidatorSetの変更がされるたびに、P-Chainは`AddressedCall`に署名し、その情報を追跡しているコントラクトへ通知できます。
受け取った側はこのメッセージを解釈し、必要な処理を実行する必要があります。
**ACP77**で定義されている`AddressedCall`は、`L1ValidatorRegistrationMessage`と`L1ValidatorWeightMessage`の2種類です。

この前提とAvalanche Mainnetで多くのチェーンがEVMベースである事実を踏まえ、`ACP99Manager`は以下の役割を果たす標準Solidityコントラクト仕様として提案されます。

| 役割 | 説明 |
| --- | ---- |
| ValidatorSetの情報保持 | 現在のL1ValidatorSetに関する情報をコントラクトに保持する  |
| P-Chainへの更新送信     | `ACP-77`で定義された`AddressedCall`を生成し、P-Chainに更新を伝える |
| 通知の解釈と反映          | P-Chainからの通知を正しく処理し、ValidatorSetを更新する  |
| 他モデルとの統合          | Proof-of-Stakeなどのセキュリティモデルに簡単に統合できる  |

監査済みかつオープンソースの参照実装を利用可能にすることで、AvalancheでL1を立ち上げるコストを下げる狙いがあります。
デプロイされた`ACP99Manager`は、`ConvertSubnetToL1Tx`で指定する`Address`としてそのまま利用できます。

## 動機

**ACP77 (Reinventing Subnets)**では、P-Chainに保存されているL1のValidatorSetを、Avalancheネットワーク上の任意のチェーンから管理できる仕組みが提案されています。
Subnetを`ConvertSubnetToL1Tx`でL1に変換するとき、`(blockchainID, address)`の形で「`ValidatorManager`」を指定でき、そのアドレスが`RegisterL1ValidatorTx`や`SetL1ValidatorWeightTx`に対応するICMメッセージをP-Chainに送ることで、Validatorの追加や重みの変更、削除を行うことができます。

また、ValidatorSetの変更がされるたびに、P-Chainは`AddressedCall`に署名し、その情報を追跡しているコントラクトへ通知します。
受け取る側はこのメッセージを正しく処理し、必要な更新を反映する必要があります。ここで利用される`AddressedCall`は**ACP77**で定義されており、種類は`L1ValidatorRegistrationMessage`と`L1ValidatorWeightMessage`です。

Avalanche Mainnetで多くのチェーンがEVMベースで動いていることから、共通して利用できるSolidity実装が必要になります。
`ACP99Manager`はその標準仕様として提案されており、以下の利点があります。

* ValidatorSetの管理機能を1つの仕様にまとめることで、実装コストを削減できる
* Proof-of-Stakeなど多様なセキュリティモデルに統合しやすくなる
* 監査済みかつオープンソースの参照実装を提供することで、L1立ち上げのハードルを下げられる

最終的には、`ACP99Manager`をデプロイし、それを`ConvertSubnetToL1Tx`の`Address`として指定するだけで、P-Chainとの連携によるValidatorSet管理が可能になります。

## 仕様

### 型定義

#### `ConversionData`

```javascript
struct ConversionData {
    bytes32 subnetID;
    bytes32 validatorManagerBlockchainID;
    address validatorManagerAddress;
    InitialValidator[] initialValidators;
}
```

SubnetをP-Chain上でL1に変換するために使われるデータを表す構造体。

このデータは、P-Chainによって認証されるハッシュの元データとなります。ValidatorManagerがこれを検証し、初期バリデータ情報を含めてL1への変換を行う際に利用されます。

**パラメータ**

* `subnetID`
    * Subnetを識別する32バイトのID。
* `validatorManagerBlockchainID`
    * ValidatorManagerが動作するブロックチェーンのID。
* `validatorManagerAddress`
    * ValidatorManagerコントラクトのアドレス。
* `initialValidators`
  * 初期バリデータのリスト。

#### `InitialValidator`

```javascript
struct InitialValidator {
    bytes nodeID;
    bytes blsPublicKey;
    uint64 weight;
}
```

初期バリデータを指定するための構造体。
SubnetをL1へ変換する時に、最初に登録されるValidatorの情報を保持します。

**パラメータ**

* `nodeID`
  * ノードを識別するID。
* `blsPublicKey`
  * ValidatorのBLS公開鍵。
* `weight`
  * Validatorの重み。

#### `ValidatorStatus`

```javascript
enum ValidatorStatus {
    Unknown,
    PendingAdded,
    Active,
    PendingRemoved,
    Completed,
    Invalidated
}
```

Validatorのステートを表す列挙型。
バリデータが登録前・稼働中・削除予定など、どの段階にあるかを示すステータスを管理します。

**パラメータ**

* `Unknown`
  * 不明な状態。
* `PendingAdded`
  * 追加待ちの状態。
* `Active`
  * 有効な状態。
* `PendingRemoved`
  * 削除待ちの状態。
* `Completed`
  * 処理が完了した状態。
* `Invalidated`
  * 無効化された状態。

#### `PChainOwner`

```javascript
struct PChainOwner {
    uint32 threshold;
    address[] addresses;
}
```

Validatorの残高所有者または無効化権限所有者を表す構造体。
P-Chainアドレスは20バイトのため、Ethereumアドレスと同じ`address`型で表現しています。

**パラメータ**

* `threshold`
  * 署名を有効とするために必要な最小署名数。
* `addresses`
  * 関連付けられたP-Chainアドレスの配列。

#### `Validator`

```javascript
struct Validator {
    ValidatorStatus status;
    bytes nodeID;
    uint64 startingWeight;
    uint64 sentNonce;
    uint64 receivedNonce;
    uint64 weight;
    uint64 startTime;
    uint64 endTime;
}
```

Validatorの稼働状態を保持する構造体。
バリデータが登録されてから削除されるまでの期間を通じて、ステータスや重み、開始・終了時間などを追跡します。

**パラメータ**

* `status`
  * Validatorの状態。
* `nodeID`
  * ノードID。
* `startingWeight`
  * 登録時の重み。
* `sentNonce`
  * Managerから送信された最新のNonce。
* `receivedNonce`
  * P-Chainから受信した最新のNonce。
* `weight`
  * 現在の重み。
* `startTime`
  * 稼働開始時間。
* `endTime`
  * 稼働終了時間。

### イベント

#### `RegisteredInitialValidator`

```javascript
event RegisteredInitialValidator(
    bytes32 indexed validationID,
    bytes20 indexed nodeID,
    bytes32 indexed subnetID,
    uint64 weight,
    uint32 index
);
```

初期バリデータが登録された時に発行されるイベント。
変換データに含まれる初期Validatorが登録された時に発行されます。
`subnetID`と`index`は、将来Validator削除時に署名リクエストの根拠として使われます。

**パラメータ**

* `validationID`
  * Validatorを識別するID。
* `nodeID`
  * ノードID。
* `subnetID`
  * SubnetのID。
* `weight`
  * Validatorの重み。
* `index`
  * 初期バリデータのインデックス。

#### `InitiatedValidatorRegistration`

```javascript
event InitiatedValidatorRegistration(
    bytes32 indexed validationID,
    bytes20 indexed nodeID,
    bytes32 registrationMessageID,
    uint64 registrationExpiry,
    uint64 weight
);
```

Validatorの登録が開始された時に発行されるイベント。
新しいValidatorの登録を試みる際に発行されます。
登録完了前はまだ有効なValidatorとはみなされません。

**パラメータ**

* `validationID`
  * Validatorを識別するID。
* `nodeID`
  * ノードID。
* `registrationMessageID`
  * 登録メッセージのID。
* `registrationExpiry`
  * 登録の有効期限。
* `weight`
  * Validatorの重み。

#### `CompletedValidatorRegistration`

```javascript
event CompletedValidatorRegistration(bytes32 indexed validationID, uint64 weight);
```

Validatorの登録が完了した時に発行されるイベント。
P-Chainによる承認を受けてValidatorが正式に登録されたことを示します。

**パラメータ**

* `validationID`
  * Validatorを識別するID。
* `weight`
  * Validatorの重み。

#### `InitiatedValidatorRemoval`

```javascript
event InitiatedValidatorRemoval(
    bytes32 indexed validationID,
    bytes32 validatorWeightMessageID,
    uint64 weight,
    uint64 endTime
);
```

Validatorの削除が開始された時に発行されるイベント。
Validatorを削除する処理が始まった時に発行されます。
まだ削除は完了していません。

**パラメータ**

* `validationID`
  * 削除対象のValidatorID。
* `validatorWeightMessageID`
  * 重み変更メッセージID。
* `weight`
  * Validatorの重み。
* `endTime`
  * 終了時間。

#### `CompletedValidatorRemoval`

```javascript
event CompletedValidatorRemoval(bytes32 indexed validationID);
```

Validatorの削除が完了した時に発行されるイベント。
P-Chainで削除が承認され、Validatorが取り除かれたことを示します。

**パラメータ**

* `validationID`
  * 削除完了したValidatorID。

#### `InitiatedValidatorWeightUpdate`

```javascript
event InitiatedValidatorWeightUpdate(
    bytes32 indexed validationID, uint64 nonce, bytes32 weightUpdateMessageID, uint64 weight
);
```

Validatorの重み更新が開始された時に発行されるイベント。

**パラメータ**

* `validationID`
  * ValidatorのID。
* `nonce`
  * 更新Nonce。
* `weightUpdateMessageID`
  * 重み更新メッセージID。
* `weight`
  * 新しい重み。

#### `CompletedValidatorWeightUpdate`

```javascript
event CompletedValidatorWeightUpdate(bytes32 indexed validationID, uint64 nonce, uint64 weight);
```

Validatorの重み更新が完了した時に発行されるイベント。
P-Chainによる承認を受け、Validatorの重みが正式に更新されたことを示します。

**パラメータ**

* `validationID`
  * ValidatorのID。
* `nonce`
  * 更新Nonce。
* `weight`
  * 新しい重み。

### 関数

#### `subnetID`

```javascript
function subnetID() public view returns (bytes32 id);
```

このManagerが管理するL1のSubnetIDを返す関数。
コントラクトに紐づくSubnetのIDを取得します。

**戻り値**

* `id`
  * SubnetのID。

#### `getValidator`

```javascript
function getValidator(bytes32 validationID)
    public
    view
    returns (Validator memory validator);
```

指定したValidatorIDに対応するValidatorの情報を返す関数。

**引数**

* `validationID`
  * Validatorを識別するID。

**戻り値**

* `validator`
  * Validator構造体。

#### `l1TotalWeight`

```javascript
function l1TotalWeight() public view returns (uint64 weight);
```

現在のL1ValidatorSet全体の合計重みを返す関数。
全てのアクティブなValidatorの重みを合算した結果を返します。

**戻り値**

* `weight`
  * 合計の重み。

#### `initializeValidatorSet`

```javascript
function initializeValidatorSet(
    ConversionData calldata conversionData,
    uint32 messsageIndex
) public;
```

初期のValidatorSetを設定する関数。
P-Chainから送られる`SubnetToL1ConversionMessage`を取り込み、L1の初期ValidatorSetを確立します。
与えられた`conversionData`を基に検証を行い、各初期Validatorについて`RegisteredInitialValidator`イベントを発行します。

**引数**

* `conversionData`
  * Subnet変換時に使用するデータで、P-Chain上の`ConversionID`と一致するか検証されます。
* `messsageIndex`
  * ICMメッセージのインデックス。
  * ここに含まれる`SubnetToL1ConversionMessage`の`ConversionID`が`conversionData`と一致する必要があります。

#### `completeValidatorRegistration`

```javascript
function completeValidatorRegistration(uint32 messageIndex)
    public
    returns (bytes32 validationID);
```

Validatorの登録を完了させる関数。
P-Chainからの`L1ValidatorRegistrationMessage`を受信して処理し、Validatorの登録を正式に完了させます。
この関数の実行が成功して初めて、Validatorはアクティブな状態とみなされます。
成功時には`CompletedValidatorRegistration`イベントが発行されます。

**引数**

* `messageIndex`
  * `L1ValidatorRegistrationMessage`のインデックス。

**戻り値**

* `validationID`
  * 登録が完了したValidatorのID。

#### `completeValidatorRemoval`

```javascript
function completeValidatorRemoval(uint32 messageIndex)
    public
    returns (bytes32 validationID);
```

Validatorの削除を完了させる関数。
P-Chainから送られる`RegisterL1ValidatorMessage`を受信し、対象のValidatorが正式に削除されたことを確認します。
成功時には`CompletedValidatorRemoval`イベントが発行されます。

**引数**

* `messageIndex`
  * 削除確認を行う`RegisterL1ValidatorMessage`のインデックス。

**戻り値**

* `validationID`
  * 削除が完了したValidatorのID。

#### `completeValidatorWeightUpdate`

```javascript
function completeValidatorWeightUpdate(uint32 messageIndex)
    public
    returns (bytes32 validationID, uint64 nonce);
```

Validatorの重み更新を完了させる関数。
P-Chainから送られる`L1ValidatorWeightMessage`を受信して処理し、Validatorの重み変更を正式に反映します。
この関数が実行されるまで、重みの変更は有効になりません。
成功時には`CompletedValidatorWeightUpdate`イベントが発行されます。

**引数**

* `messageIndex`
  * `L1ValidatorWeightMessage`のインデックス。

**戻り値**

* `validationID`
  * 更新されたValidatorのID。
* `nonce`
  * 更新に対応するNonce値。

### 内部関数

#### `_initiateValidatorRegistration`

```javascript
function _initiateValidatorRegistration(
    bytes memory nodeID,
    bytes memory blsPublicKey,
    PChainOwner memory remainingBalanceOwner,
    PChainOwner memory disableOwner,
    uint64 weight
) internal returns (bytes32 validationID);
```

Validator登録を開始する内部関数。
新しいValidatorを追加するための`RegisterL1ValidatorMessage`を発行します。
この関数を呼び出しただけではValidatorはまだアクティブではなく、後で`completeValidatorRegistration`が呼ばれる必要があります。
成功すると`InitiatedValidatorRegistration`イベントが発行されます。

**引数**

* `nodeID`
  * 登録するノードのID。
* `blsPublicKey`
  * ValidatorのBLS公開鍵。
* `remainingBalanceOwner`
  * 残高所有者情報。
* `disableOwner`
  * 無効化権限所有者情報。
* `weight`
  * ノードの重み。

**戻り値**

* `validationID`
  * 登録されたValidatorのID。

#### `_initiateValidatorRemoval`

```javascript
function _initiateValidatorRemoval(bytes32 validationID) internal;
```

Validator削除を開始する内部関数。
対象のValidatorについて、重みを0にする`L1ValidatorWeightMessage`を発行し削除プロセスを開始します。
この呼び出し直後に対象のValidatorは非アクティブとみなされます。
成功時には`InitiatedValidatorRemoval`イベントが発行されます。

**引数**

* `validationID`
  * 削除対象のValidatorID。

#### `_initiateValidatorWeightUpdate`

```javascript
function _initiateValidatorWeightUpdate(
    bytes32 validationID,
    uint64 weight
) internal returns (uint64 nonce, bytes32 messageID);
```

Validatorの重み更新を開始する内部関数。
指定したValidatorについて新しい重みを設定する`L1ValidatorWeightMessage`を発行します。
更新はこの関数実行直後にはまだ有効にならず、`completeValidatorWeightUpdate`の呼び出しによって正式に反映されます。
成功時には`InitiatedValidatorWeightUpdate`イベントが発行されます。

**引数**

* `validationID`
  * 重みを変更するValidatorのID。
* `weight`
  * 新しい重み。

**戻り値**

* `nonce`
  * この更新に対応するNonce。
* `messageID`
  * 重み更新メッセージのID。

### `DisableL1ValidatorTx` について

Validatorは、上記の削除プロセスとは別に、P-Chainで`DisableL1ValidatorTx`を発行して直接無効化することができます。

これは、Validatorの`DisableOwner`が実行できる特権トランザクションであり、失敗したL1からValidatorに紐づく残高を回収するために利用されます。
そのため、通常の`ACP99Manager`コントラクトから呼び出すものではありません。

## 互換性

`ACP99Manager`はAvalancheプロトコルの現在の挙動には影響しません。
既存ノードや既存チェーンの設定変更やネットワークアップグレードを前提とせず、実装者がこの仕様に沿った`ACP99Manager`コントラクトを採用するかどうかは各プロジェクトの判断に委ねられます。

## 参考実装

AvaLabsの[ICMContracts](https://github.com/ava-labs/icm-contracts/tree/main/contracts/validator-manager)リポジトリで参照実装が提供されます。
この参照実装は、`ACP99Manager`仕様に整合するように更新される予定であり、その更新が完了してはじめて本ACPを`Implementable`（実装可能）とマークできます。

#### 例：統合アーキテクチャ

`ACP99Manager`はどのようなアーキテクチャにも組み込みやすい設計です。
**ACP99**では、異なるアーキテクチャ方針を示す2つの例を提示しています。

##### マルチコントラクト設計

この設計では、`ACP99Manager`を実装するコントラクトと、PoSやPoAなどのセキュリティモデルを担う「**セキュリティモジュール**」コントラクトを分離します。
各`ACP99Manager`実装は、1つ以上のセキュリティモジュールと関連づけられ、**ValidatorSet変更を開始する関数**（`initiateValidatorRegistration`、`initiateValidatorWeightUpdate`）を呼べるのはセキュリティモジュールのみとします。
Validatorの追加・削除・重み変更が開始されるたびに、`ACP99Manager`実装は対応するセキュリティモジュールの関数（`handleValidatorRegistration`または`handleValidatorWeightChange`）を呼び出します。
安全性の観点から、セキュリティモジュールは不変の`ACP99Manager`アドレスを参照することが推奨されます。

セキュリティモジュールが、P-Chainで確定した「**追加／削除／重み変更**」を受けてどのようなアクションを取るかはモジュール側の設計に委ねられます（例：PoSL1における出金待機期間の開始、報酬の割当など）。

```mermaid
---
title: ACP-99 Multi-contract Architecture with a PoA Security Module
---
graph LR
  Safe(Safe multisig)
  SecurityModule(ACP99PoAModule)
  Manager(ACP99Manager)
  P(P-Chain)

  subgraph "Manager chain"
    Safe
    SecurityModule
    Manager
  end
  Safe -.->|Own| SecurityModule
  Safe -.->|Own| Manager
  SecurityModule <-.->|Reference| Manager
  Safe -->|addValidator| SecurityModule
  SecurityModule -->|initiateValidatorRegistration| Manager
  Manager -->|sendWarpMessage| P
  P -->|completeValidatorRegistration| Manager
  Manager -->|handleValidatorRegistration| SecurityModule
```

この設計を採る「**セキュリティモジュール**」は、PoS、LiquidPoSなど様々な方式を実装可能です（これらモジュールの仕様は本ACPの範囲外です）。
進行中の実装は[SuzakuContractsLibrary](https://github.com/suzaku-network/suzaku-contracts-library/blob/main/README.md#acp99-contracts-library)にあり、議論の結果に基づき`Implementable`と見なされるまで更新されます。
AvaLabsのV2ValidatorManagerもこの構成でPoSモジュールを実装しており、[ICMContractsRepository](https://github.com/ava-labs/icm-contracts/tree/validator-manager-v2.0.0/contracts/validator-manager/StakingManager.sol)で公開されています。

##### シングルコントラクト設計

この設計では、基底クラスが`ACP99Manager`を実装し、その子クラスで特定のセキュリティモデル（例：PoA）を実装します。
下図では`PoAValidatorManager`が子クラス例ですが、PoSなど別モデルを実装する子クラスに差し替えることもできます。

```mermaid
---
title: ACP-99 Single-Contract Architecture implementing PoA
---
  classDiagram
  class ACP99Manager {
    initializeValidatorSet
    initiateValidatorRegistration
    completeValidatorRegistration
    initiateValidatorWeightUpdate
    completeValidatorWeightUpdate
  }
  <<abstract>> ACP99Manager

  class ValidatorManager {
    completeValidatorRegistration
  }
  <<abstract>> ValidatorManager
  class PoAValidatorManager {
    initiateValidatorRegistration
    initiateEndValidation
    completeEndValidation
  }

  ACP99Manager <|--ValidatorManager
  ValidatorManager <|-- PoAValidatorManager
```

この構成に特化した参照実装は提供されていませんが、AvaLabsのV1[ValidatorManager](https://github.com/ava-labs/icm-contracts/tree/validator-manager-v1.0.0/contracts/validator-manager)は、**ACP99**で説明される機能の多くを実装しています。
ただし本仕様の策定前に作られたため相違点があり、「**標準の概念実装モデル**」として参照するにとどめるべきとされています。

## セキュリティ

`ACP99Manager`および参照実装の監査は、Avalancheエコシステムの将来にとって重要です。
多くのL1がこれに依存して自身の安全性を確保することになるため、仕様適合性と実装の健全性の確認は最優先で実施されるべきです。
設計・実装・運用の各段階でのレビューと検証を通じて、想定外の挙動や脆弱性の混入を防ぐことが求められます。

## Open Questions

#### マネージャーチェーン上でバリデータ集合の履歴情報を保持すべきか

`getValidator`関数で過去のバリデータ情報を返すべきかどうかは、実装者の判断に委ねられています。

過去のバリデータの稼働実績や重みの推移などの情報が有益である場合もありますが、すべてのアプリケーションに必要とは限りません。
例えば、PoA（Proof-of-Authority）モデルでは過去のバリデータ稼働率を参照する必要がなく、履歴保持の意義は薄いと考えられます。

履歴情報そのものは、アーカイブノードやエクスプローラーなどのオフチェーンツールで取得可能です。
そのため、コントラクトレベルで必ず保持する必要はなく、必要に応じて実装されるべき性質のものです。

#### `ACP99Manager`に「churn control（入れ替え制御）」機能を含めるべきか

AvaLabsが提供する[ValidatorManager実装](https://github.com/ava-labs/icm-contracts/blob/main/contracts/validator-manager/ValidatorManager.sol)には「churn control」機能が含まれています。

これは、短時間に大量の重みが追加・削除されることを防止する仕組みです。バリデータの入れ替えや重み変更が急激に行われると、コンセンサスに失敗するリスクが高まります。
そのため、一定の範囲内でしか変更を許さないように制御することは、安定性確保の観点で有効と考えられています。

一方で、すべての実装やユースケースに必須とは限りません。
PoAのようなモデルでは大規模な入れ替え制御が不要な場合もあり、設計の柔軟性を残すべきとの考え方もあります。
したがって、この機能を必須とするかどうかは依然として議論中です。

## 最後に

今回は「AvalancheのEVMチェーンでL1のバリデータ集合（追加・削除・重み変更）を管理するためのコントラクトを提案している**ACP99**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください！

[Twitter @cardene777](https://twitter.com/cardene777)

他の媒体でも情報発信しているのでぜひ他も見ていってください！

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
