はじめに

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

今回は、コントラクトのUpgradeに遅延時間を設ける仕組みを提案しているERC3561についてまとめていきます!

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

他にも様々なEIPについてまとめています。

概要

ERC3561では、アップグレード可能なプロキシにおいて、信頼を前提としない設計を実現する提案しています。 特に、匿名の開発者がプロキシのアップグレード機能を利用する時に、外部からの信頼を得にくいという課題を解消するために設計されています。 提案された仕組みでは、アップグレードを即時に反映することを防ぎ、「Zero Trust Period(ゼロトラスト期間)」が経過した後にのみ、アップグレードが有効になるよう制御されます。

この仕組みにより、意図的または悪意のある即時アップグレードを防止し、より信頼性の高いスマートコントラクト運用を目指します。 これを実現するために、アップグレード処理に関連する追加のストレージスロットを設ける設計が導入されます。

動機

現在、多くの匿名開発者がアップグレード可能なプロキシを利用してスマートコントラクトを開発していますが、コミュニティからの信頼を得ることは難しいのが現状です。 一方で、Upgradeableは、イノベーションを迅速に反映して脆弱性に対応するために重要です。

このような状況の中で、ERC3561で提案されている仕組みは、匿名性を維持しながらも信頼を得るための新たな手段を提供します。 ゼロトラスト期間を設けることで、開発者が即座に悪意あるアップグレードを行うことを防ぎ、外部からの信頼を構築しやすくします。 これは、公平で持続可能な未来を実現するために、匿名開発者の存在が不可欠であるという信念に基づいています。

仕様

Nextロジックコントラクト

js
// Sets next logic contract address. Emits NextLogicDefined
// If current implementation is address(0), then upgrades to IMPLEMENTATION_SLOT
// immedeatelly, therefore takes data as an argument
function proposeTo(address implementation, bytes calldata data) external IfAdmin
// As soon UPGRADE_BLOCK_SLOT allows, sets the address stored as next implementation
// as current IMPLEMENTATION_SLOT and initializes it.
function upgrade(bytes calldata data) external IfAdmin
// cancelling is possible for as long as upgrade() for given next logic was not called
// emits NextLogicCanceled
function cancelUpgrade() external onlyAdmin;

event NextLogicDefined(address indexed nextLogic, uint earliestArrivalBlock); // important to have
event NextLogicCanceled(address indexed oldLogic);

NextLogicDefined

javascript
event NextLogicDefined(address indexed nextLogic, uint earliestArrivalBlock);

次に適用されるロジックコントラクトのアドレスと、アップグレード可能になる最も早いブロック番号を通知するイベント。 このイベントは、proposeTo関数により新しいロジックが提案された時に発行されます。

パラメータ

  • nextLogic
    • 新たに提案されたロジックコントラクトのアドレス。
  • earliestArrivalBlock
    • アップグレードが可能となる最も早いブロック番号。

NextLogicCanceled

javascript
event NextLogicCanceled(address indexed oldLogic);

提案されたロジックのアップグレードがキャンセルされたことを通知するイベント。 cancelUpgrade関数により、まだ適用されていない提案済みロジックのアップグレードを取り消した際に発行されます。

パラメータ

  • oldLogic
    • キャンセルされたロジックのアドレス。

onlyAdmin

js
modifier onlyAdmin;

管理者(Admin)による操作に制限をかける修飾子。 この修飾子が付与された関数は、コントラクトのAdmin権限を持つアドレスのみが実行できます。 信頼性のある操作管理のために必須です。

proposeTo

js
function proposeTo(address implementation, bytes calldata data) external onlyAdmin;

新たなロジックコントラクトを提案する関数。 指定したアドレスを次に使用するロジックとして設定します。 もし現在の実装が未設定(address(0))であれば、その場で即座にアップグレードが実行されます。 通常は、後述のupgrade関数を通じて、ゼロトラスト期間後に反映されます。

引数

  • implementation
    • 提案されるロジックコントラクトのアドレス。
  • data
    • 初期化のための呼び出しデータ(upgrade時に使われるものと同等)。

upgrade

js
function upgrade(bytes calldata data) external onlyAdmin;

proposeToで設定されたロジックを正式に適用する関数です。

詳細

設定されたゼロトラスト期間(earliestArrivalBlock)が経過していれば、ERC1967準拠のIMPLEMENTATION_SLOTにロジックアドレスが反映され、コントラクトの挙動が新たな実装に切り替わります。

ERC1967については以下の記事を参考にしてください。

引数

  • data
    • アップグレード後に実行される初期化処理の呼び出しデータ。

cancelUpgrade

js
function cancelUpgrade() external onlyAdmin;

提案されたロジックのアップグレードをキャンセルする関数。 proposeToによって提案されたロジックが、まだupgradeによって適用されていない状態でのみ実行可能です。 不要となったアップグレード提案を破棄するために使用されます。

Propose

js
function prolongLock(uint b) external onlyAdmin;
event ProposingUpgradesRestrictedUntil(uint block, uint nextProposedLogicEarliestArrival);

ProposingUpgradesRestrictedUntil

javascript
event ProposingUpgradesRestrictedUntil(uint block, uint nextProposedLogicEarliestArrival);

次のロジック提案が可能になるまでのブロック制限と、そのロジックが適用可能になる最も早いブロック番号を通知するイベント。 prolongLock関数の実行時に発行されます。

パラメータ

  • block
    • 次回の提案が可能になるブロック番号。
  • nextProposedLogicEarliestArrival
    • 提案されたロジックが有効になる最も早いブロック番号。

prolongLock

js
function prolongLock(uint b) external onlyAdmin;

アップグレードの提案自体を将来のブロックまで禁止する関数。 これにより、一時的または恒久的にアップグレード提案を制限することができます。 極端な場合は、bにuint256の最大値を設定することで完全にアップグレードを封じることも可能です。

引数

  • b
    • 次のアップグレード提案が許可されるブロック番号。

ZeroTrust期間

js
function setZeroTrustPeriod(uint blocks) external onlyAdmin;
event ZeroTrustPeriodSet(uint blocks);

ZeroTrustPeriodSet

javascript
event ZeroTrustPeriodSet(uint blocks);

ゼロトラスト期間が設定・更新されたことを通知するイベント。 setZeroTrustPeriod関数の実行時に発行されます。 この期間は、ロジックの変更に対する猶予期間であり、即時変更を防ぐ仕組みの中核となります。

パラメータ

  • blocks
    • 設定されたゼロトラスト期間(ブロック数)。

setZeroTrustPeriod

js
function setZeroTrustPeriod(uint blocks) external onlyAdmin;

ゼロトラスト期間を設定・更新する関数。 この期間は、ロジックコントラクトの変更に対して強制的な猶予を設けることで、信頼性を向上させます。 一度設定された後は、同じかそれ以上の期間にしか更新できません。

引数

  • blocks
    • 設定するゼロトラスト期間(ブロック数)。

参考実装

TrustMinimizedProxy.sol
pragma solidity >=0.8.0; //important

// EIP-3561 trust minimized proxy implementation https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3561.md
// Based on EIP-1967 upgradeability proxy: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1967.md

contract TrustMinimizedProxy {
    event Upgraded(address indexed toLogic);
    event AdminChanged(address indexed previousAdmin, address indexed newAdmin);
    event NextLogicDefined(address indexed nextLogic, uint earliestArrivalBlock);
    event ProposingUpgradesRestrictedUntil(uint block, uint nextProposedLogicEarliestArrival);
    event NextLogicCanceled();
    event ZeroTrustPeriodSet(uint blocks);

    bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
    bytes32 internal constant LOGIC_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 internal constant NEXT_LOGIC_SLOT = 0x19e3fabe07b65998b604369d85524946766191ac9434b39e27c424c976493685;
    bytes32 internal constant NEXT_LOGIC_BLOCK_SLOT = 0xe3228ec3416340815a9ca41bfee1103c47feb764b4f0f4412f5d92df539fe0ee;
    bytes32 internal constant PROPOSE_BLOCK_SLOT = 0x4b50776e56454fad8a52805daac1d9fd77ef59e4f1a053c342aaae5568af1388;
    bytes32 internal constant ZERO_TRUST_PERIOD_SLOT = 0x7913203adedf5aca5386654362047f05edbd30729ae4b0351441c46289146720;

    constructor() payable {
        require(
            ADMIN_SLOT == bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) &&
                LOGIC_SLOT == bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) &&
                NEXT_LOGIC_SLOT == bytes32(uint256(keccak256('eip3561.proxy.next.logic')) - 1) &&
                NEXT_LOGIC_BLOCK_SLOT == bytes32(uint256(keccak256('eip3561.proxy.next.logic.block')) - 1) &&
                PROPOSE_BLOCK_SLOT == bytes32(uint256(keccak256('eip3561.proxy.propose.block')) - 1) &&
                ZERO_TRUST_PERIOD_SLOT == bytes32(uint256(keccak256('eip3561.proxy.zero.trust.period')) - 1)
        );
        _setAdmin(msg.sender);
    }

    modifier IfAdmin() {
        if (msg.sender == _admin()) {
            _;
        } else {
            _fallback();
        }
    }

    function _logic() internal view returns (address logic) {
        assembly {
            logic := sload(LOGIC_SLOT)
        }
    }

    function _nextLogic() internal view returns (address nextLogic) {
        assembly {
            nextLogic := sload(NEXT_LOGIC_SLOT)
        }
    }

    function _proposeBlock() internal view returns (uint b) {
        assembly {
            b := sload(PROPOSE_BLOCK_SLOT)
        }
    }

    function _nextLogicBlock() internal view returns (uint b) {
        assembly {
            b := sload(NEXT_LOGIC_BLOCK_SLOT)
        }
    }

    function _zeroTrustPeriod() internal view returns (uint ztp) {
        assembly {
            ztp := sload(ZERO_TRUST_PERIOD_SLOT)
        }
    }

    function _admin() internal view returns (address adm) {
        assembly {
            adm := sload(ADMIN_SLOT)
        }
    }

    function _setAdmin(address newAdm) internal {
        assembly {
            sstore(ADMIN_SLOT, newAdm)
        }
    }

    function changeAdmin(address newAdm) external IfAdmin {
        emit AdminChanged(_admin(), newAdm);
        _setAdmin(newAdm);
    }

    function upgrade(bytes calldata data) external IfAdmin {
        require(block.number >= _nextLogicBlock(), 'too soon');
        address logic;
        assembly {
            logic := sload(NEXT_LOGIC_SLOT)
            sstore(LOGIC_SLOT, logic)
        }
        (bool success, ) = logic.delegatecall(data);
        require(success, 'failed to call');
        emit Upgraded(logic);
    }

    fallback() external payable {
        _fallback();
    }

    receive() external payable {
        _fallback();
    }

    function _fallback() internal {
        require(msg.sender != _admin());
        _delegate(_logic());
    }

    function cancelUpgrade() external IfAdmin {
        address logic;
        assembly {
            logic := sload(LOGIC_SLOT)
            sstore(NEXT_LOGIC_SLOT, logic)
        }
        emit NextLogicCanceled();
    }

    function prolongLock(uint b) external IfAdmin {
        require(b > _proposeBlock(), 'can be only set higher');
        assembly {
            sstore(PROPOSE_BLOCK_SLOT, b)
        }
        emit ProposingUpgradesRestrictedUntil(b, b + _zeroTrustPeriod());
    }

    function setZeroTrustPeriod(uint blocks) external IfAdmin {
        // before this set at least once acts like a normal eip 1967 transparent proxy
        uint ztp;
        assembly {
            ztp := sload(ZERO_TRUST_PERIOD_SLOT)
        }
        require(blocks > ztp, 'can be only set higher');
        assembly {
            sstore(ZERO_TRUST_PERIOD_SLOT, blocks)
        }
        _updateNextBlockSlot();
        emit ZeroTrustPeriodSet(blocks);
    }

    function _updateNextBlockSlot() internal {
        uint nlb = block.number + _zeroTrustPeriod();
        assembly {
            sstore(NEXT_LOGIC_BLOCK_SLOT, nlb)
        }
    }

    function _setNextLogic(address nl) internal {
        require(block.number >= _proposeBlock(), 'too soon');
        _updateNextBlockSlot();
        assembly {
            sstore(NEXT_LOGIC_SLOT, nl)
        }
        emit NextLogicDefined(nl, block.number + _zeroTrustPeriod());
    }

    function proposeTo(address newLogic, bytes calldata data) external payable IfAdmin {
        if (_zeroTrustPeriod() == 0 || _logic() == address(0)) {
            _updateNextBlockSlot();
            assembly {
                sstore(LOGIC_SLOT, newLogic)
            }
            (bool success, ) = newLogic.delegatecall(data);
            require(success, 'failed to call');
            emit Upgraded(newLogic);
        } else {
            _setNextLogic(newLogic);
        }
    }

    function _delegate(address logic_) internal {
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), logic_, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }
}

補足

「最初からアップグレード可能にしなければよい」という意見は一見正論のように思えますが、現実的には複雑なシステムに対しては不適切です。 特に人間の判断や運用に依存する部分があるプロトコルでは、初回の設計段階で完璧なモデルを構築することは困難です。

ERC1967プロキシと分散型ガバナンスによるアップグレード管理の組み合わせは、プロトコルが成熟して十分なデータが揃うまでの期間において、重大なボトルネックになり得ます。

ERC3561では、ロジックのアップグレードに一定の遅延(タイムラグ)を設けることにより、即時アップグレードによる悪用のリスクを防ぎます。 例えば初心者開発者が使用に躊躇するような設計であっても、現在のスマートコントラクト開発においては、こうした信頼最小化オプションを用意することが重要です。

セキュリティ

このゼロトラスト・プロキシを使用する時には、ユーザー自身が以下の点を十分に確認する必要があります。

  • オーバーフローなどの脆弱性がないこと。
  • 提案された仕様に準拠したコード(例:実装サンプルと同一の構造)であること。
  • ゼロトラスト期間が十分に長く設定されていること。

ゼロトラスト期間については、通常アップグレードの内容が事前に公表される場合でも最低2週間、そうでない場合は少なくとも1か月以上が望ましいとされています。 この期間が短すぎると、ユーザーが検証・対応する余地がなくなり、悪意あるアップグレードに対する防御が不十分になります。

引用

Sam Porter (@SamPorter1984), "ERC-3561: Trust Minimized Upgradeability Proxy [DRAFT]," Ethereum Improvement Proposals, no. 3561, May 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3561.

最後に

今回は「コントラクトのUpgradeに遅延時間を設ける仕組みを提案しているERC3561」についてまとめてきました! いかがだったでしょうか?

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

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