はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、提案を作成し実行するためにインターフェースを提案している規格であるERC5247についてまとめていきます!

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

5247は現在(2023年10月5日)では「Review」段階です。

概要

このEIP(Ethereum Improvement Proposal)は、ブロックチェーン上のスマートコントラクトに関する提案を作成し、実行するための方法を提供しています。 スマートコントラクトは、ブロックチェーン上で自動的に実行されるプログラムです。 この提案を使うことで、以下の情報を含む情報をブロックチェーンに送信できます。

  • ターゲットコントラクトのアドレス

    • どのスマートコントラクトに対して提案を実行するかを示すアドレス。
    • 提案の対象となるスマートコントラクトを指定します。
  • 送金するEtherの量

    • 提案に関連するEther(仮想通貨)の金額を指定します。
    • スマートコントラクト内でトークンを売買する時などに送金されることがあります。
  • ガス制限

    • 提案の実行に使用できるガスの制限を設定します。
    • ガスは、トランザクションを処理するために必要なリソースを表します。
    • ガス制限を適切に設定することで、トランザクションが無限ループに陥るなどの問題を防ぎます。
  • コールデータ

    • 提案に関連する関数呼び出しに必要なデータを含みます。
    • スマートコントラクトの関数は、特定のデータと引数を受け取って実行されます。
    • このコールデータは、関数がどのように実行されるかを指定します。

この提案を使用すると、ブロックチェーン上で特定のアクションを実行するための計画を立て、実行するための情報を提供できます。 これは、分散型アプリケーション(DApps)やスマートコントラクトにおいて、プログラムが自動的に実行される際に必要な機能です。

動機

この提案(EIP)の中心的なアイデアは、コードの作成と実行を分けることが必要な場合があるということです。 以下は、この提案が使われる一般的な状況です。

  • 分散型自治組織(DAO)

    • ある人がスマート提案を作成し、それを提出します。
    • メンバーはその提案を支持するかどうかを選択し、それに基づいて投票します(ERC1202を参照)。
    • 最終的に合意が形成された場合、提案が実行されます。
  • 信頼できる代理人

    • ある人が信頼できる代理人、信託者、または法的代理人などと提携してスマート提案を作成します。
    • この提案は議論され、ある方法で承認され、最終的に実行されます。
  • 提案を受け入れる提案:

    • 一方の人が、条件付きで、もう一方の人に「提案」をすることができます。
    • スマート提案は提案として提示され、もう一方の人はこの提案を受け入れるかどうかを選択し、受け入れる場合は実行できます。

AliceとBobという2人の友人がいます。 AliceはBobに対して、特定の条件を満たすときにお金を送る提案をしたいと考えています。

  1. 提案の作成

    • Aliceはスマートコントラクトを使って、Bobに対する提案を作成します。
    • この提案には次のような条件が含まれます:
      • 条件1
        • Bobが特定の仕事を完了した場合(例:庭を手入れすること)。
      • 条件2
        • 提案に含まれている金額(Ether)をBobが受け取る権利がある。
  2. 提案の提示

    • Aliceはこの提案をBobに提示します。
    • Bobは提案をスマートコントラクト上で確認できます。
    • 提案にはどの仕事が必要で、いくらの報酬が用意されているかが記載されています。
  3. 提案の受け入れ

    • Bobは提案を検討し、条件を確認します。
    • 庭を手入れすることが条件で、彼がそれを受け入れることを決定しました。
    • Bobはスマートコントラクト上で提案を受け入れます。
    • これにより、提案に含まれる金額が彼のアカウントに送金されることが確約されます。
  4. 提案の実行

    • Bobが条件を満たし、提案を受け入れた後、スマートコントラクトは自動的に金額をBobのアカウントに送金します。
    • 庭の手入れが完了したことをスマートコントラクトが確認し、支払いが実行されます。

このように、スマート提案を使うことで、条件を伴った提案をブロックチェーン上で信頼性を持って実行できます。 受け入れ側は提案に同意する前に条件を確認し、条件が満たされれば自動的に実行される仕組みです。

この提案は、コードを作成するプロセスと実際にそのコードを実行するプロセスを切り離し、異なるシナリオで提案と実行を関連付けるための仕組みです。 これにより、分散型の決定プロセスやコントラクトの実行をブロックチェーン上で効果的に管理できます。

仕様

javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

interface IERC5247 {
    event ProposalCreated(
        address indexed proposer,
        uint256 indexed proposalId,
        address[] targets,
        uint256[] values,
        uint256[] gasLimits,
        bytes[] calldatas,
        bytes extraParams
    );

    event ProposalExecuted(
        address indexed executor,
        uint256 indexed proposalId,
        bytes extraParams
    );

    function createProposal(
        uint256 proposalId,
        address[] calldata targets,
        uint256[] calldata values,
        uint256[] calldata gasLimits,
        bytes[] calldata calldatas,
        bytes calldata extraParams
    ) external returns (uint256 registeredProposalId);

    function executeProposal(uint256 proposalId, bytes calldata extraParams) external;
}

補足

最初、このインターフェースはERC1202という規格の一部でした。 ERC1202は、ブロックチェーン上での投票を管理するためのもので、特定の提案に対して投票する仕組みを提供していました。

しかし、後に提案自体が投票に限らず、さまざまな用途に適用できる可能性があることに気づきました。 つまり、提案を特定の形式に結びつけずに使いたい場合もあるということです。 そのため、提案のインターフェースと投票のインターフェースを分離する決定が行われました。

この変更に伴い、以下の2つの主要な変更がありました。

  1. 配列の使用

    • 以前は、各提案に関連するターゲット、値、コールデータなどの情報が単一の変数で管理されていました。
    • しかし、これを配列として管理することにより、提案が複数の操作を含む場合でも対応できるようになりました。
    • つまり、1つの提案で複数のアクションを実行できるようになりました。
  2. 提案IDのカスタマイズ可能性

    • registeredProposalIdという新しい要素が導入されました。
    • これはcreateProposal関数から返され、異なるプロジェクトやアプリケーションで提案IDのフォーマットを自由にカスタマイズできるようになりました。
    • これにより、提案の一意性を保ちつつ、プロジェクトに特有のIDフォーマットを使用できるようになりました。

この変更により、提案と投票の関連性を維持しつつ、提案が投票以外の目的で使われる柔軟性が向上しました。 これにより、異なるプロジェクトやユースケースに合わせた提案管理が容易になりました。

テスト

javascript
it("Should work for a simple case", async function () {
    const { contract, erc721, owner } = await loadFixture(deployFixture);
    const callData1 = erc721.interface.encodeFunctionData("mint", [owner.address, 1]);
    const callData2 = erc721.interface.encodeFunctionData("mint", [owner.address, 2]);
    await contract.connect(owner)
        .createProposal(
            0,
            [erc721.address, erc721.address],
            [0,0],
            [0,0],
            [callData1, callData2],
            []);
    expect(await erc721.balanceOf(owner.address)).to.equal(0);
    await contract.connect(owner).executeProposal(0, []);
    expect(await erc721.balanceOf(owner.address)).to.equal(2);
});

以下にテストの全体コードを格納しています。

参考実装

javascript
function createProposal(
    uint256 proposalId,
    address[] calldata targets,
    uint256[] calldata values,
    uint256[] calldata gasLimits,
    bytes[] calldata calldatas,
    bytes calldata extraParams
) external returns (uint256 registeredProposalId) {
    require(targets.length == values.length, "GeneralForwarder: targets and values length mismatch");
    require(targets.length == gasLimits.length, "GeneralForwarder: targets and gasLimits length mismatch");
    require(targets.length == calldatas.length, "GeneralForwarder: targets and calldatas length mismatch");
    registeredProposalId = proposalCount;
    proposalCount++;

    proposals[registeredProposalId] = Proposal({
        by: msg.sender,
        proposalId: proposalId,
        targets: targets,
        values: values,
        calldatas: calldatas,
        gasLimits: gasLimits
    });
    emit ProposalCreated(msg.sender, proposalId, targets, values, gasLimits, calldatas, extraParams);
    return registeredProposalId;
}
function executeProposal(uint256 proposalId, bytes calldata extraParams) external {
    Proposal storage proposal = proposals[proposalId];
    address[] memory targets = proposal.targets;
    string memory errorMessage = "Governor: call reverted without message";
    for (uint256 i = 0; i < targets.length; ++i) {
        (bool success, bytes memory returndata) = proposal.targets[i].call{value: proposal.values[i]}(proposal.calldatas[i]);
        Address.verifyCallResult(success, returndata, errorMessage);
    }
    emit ProposalExecuted(msg.sender, proposalId, extraParams);
}

以下に参考実装の全体コードを格納しています。

proposals

javascript
mapping(uint256 => Proposal) public proposals;

概要 ブロックチェーン上で提案を格納するためのデータ構造。

詳細 ブロックチェーン上のスマートコントラクトは、提案を格納するためのデータ構造が必要です。 このデータ構造をマッピングとして宣言し、提案のID(整数)をキーにして、それぞれの提案(Proposal)を関連付けています。 このようにして、提案を一元管理できます。


proposalCount

javascript
uint256 private proposalCount;

概要 提案の総数をカウントするための変数。 この変数は外部から直接アクセスできません。

詳細 提案の数を正確に追跡するために、このプライベートな変数が使用されます。 この変数を増やすことで、新しい提案が追加されたことを記録し、提案の一意のIDを生成するのに役立ちます。


createProposal

javascript
function createProposal(
    uint256 proposalId,
    address[] calldata targets,
    uint256[] calldata values,
    uint256[] calldata gasLimits,
    bytes[] calldata calldatas,
    bytes calldata extraParams
) external returns (uint256 registeredProposalId) {
    require(targets.length == values.length, "GeneralForwarder: targets and values length mismatch");
    require(targets.length == gasLimits.length, "GeneralForwarder: targets and gasLimits length mismatch");
    require(targets.length == calldatas.length, "GeneralForwarder: targets and calldatas length mismatch");
    registeredProposalId = proposalCount;
    proposalCount++;

    proposals[registeredProposalId] = Proposal({
        by: msg.sender,
        proposalId: proposalId,
        targets: targets,
        values: values,
        calldatas: calldatas,
        gasLimits: gasLimits
    });
    emit ProposalCreated(msg.sender, proposalId, targets, values, gasLimits, calldatas, extraParams);
    return registeredProposalId;
}

概要 新しい提案を作成し、登録する関数。 提案はスマートコントラクトに送信され、提案の詳細情報が提供されます。

詳細

  • 新しい提案を作成し、提案を登録します。
  • 提案ID、実行対象のアドレス、送信するEtherの量、ガス制限、コールデータなどが関数の引数として提供されます。
  • 関数内で、提案の整合性が確認され、提案IDが割り当てられます。
  • その後、提案がproposalsマッピングに登録され、提案の詳細情報が格納されます。
  • 提案の登録が完了すると、ProposalCreatedイベントが発行され、提案の作成が記録されます。

引数

  • proposalId
    • 提案の一意のID。
  • targets
    • 実行対象のアドレスの配列。
  • values
    • 送信するEtherの量の配列。
  • gasLimits
    • ガス制限の配列。
  • calldatas
    • コールデータの配列。
  • extraParams
    • 追加のパラメータ(任意)。

戻り値

  • registeredProposalId
    • 登録された提案の一意のID。

executeProposal

javascript
function executeProposal(uint256 proposalId, bytes calldata extraParams) external {
    Proposal storage proposal = proposals[proposalId];
    address[] memory targets = proposal.targets;
    string memory errorMessage = "Governor: call reverted without message";
    for (uint256 i = 0; i < targets.length; ++i) {
        (bool success, bytes memory returndata) = proposal.targets[i].call{value: proposal.values[i]}(proposal.calldatas[i]);
        Address.verifyCallResult(success, returndata, errorMessage);
    }
    emit ProposalExecuted(msg.sender, proposalId, extraParams);
}

概要 関数は提案を実行する関数。

詳細

  • 提案の実行を担当します。
  • 提案IDを指定し、その提案内に含まれるアクションを実行します。
  • アクションは指定されたアドレスに対してEtherを送信し、コールデータを渡します。
  • アクションの実行中にエラーが発生した場合、エラーメッセージが記録されます。
  • 提案の実行が完了すると、ProposalExecutedイベントが発行され提案の実行が記録されます。

引数

  • proposalId
    • 実行する提案の一意のID。
  • extraParams
    • 追加のパラメータ(任意)。

戻り値

  • なし

getProposal

javascript
function getProposal(uint256 proposalId) external view returns (Proposal memory) {
    return proposals[proposalId];
}

概要 指定された提案の詳細情報を取得する関数。

詳細

  • 提案の一意のIDを指定し、その提案の詳細情報を取得します。
  • 詳細情報には提案者、提案ID、実行対象のアドレス、送信するEtherの量、ガス制限、コールデータなどが含まれます。

引数

  • proposalId
    • 取得する提案の一意のID。

戻り値

  • Proposal
    • 提案の詳細情報を格納した構造体。

getProposalCount

javascript
function getProposalCount() external view returns (uint256) {
    return proposalCount;
}

概要 登録された提案の総数を取得する関数。

詳細

  • 登録された提案の総数を取得します。
  • 提案の数を把握するのに役立ちます。

戻り値

  • uint256
    • 登録された提案の総数。

セキュリティ考慮事項

議論の必要があります。

引用

Zainan Victor Zhou (@xinbenlv), "ERC-5247: Smart Contract Executable Proposal Interface [DRAFT]," Ethereum Improvement Proposals, no. 5247, July 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5247.

最後に

今回は「提案を作成し実行するためにインターフェースを提案している規格であるERC5247」についてまとめてきました! いかがだったでしょうか?

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

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