🤖

💎🎉 OpenZeppelin Clones 解説 🎉💎

2022/02/19に公開

🌞 概要

OpenZeppelinのUpgradeable Contractsに関するドキュメントを読んでいて、Clonesの存在を知りました。ClonesはUpgradable Contractsの仕組みの一部であるProxyの一種で、Implementation Contractの更新機能を省いたProxyだと言えます。(つまり、Implementation Contractの初期化だけできます。)
Clonesは、同じContractのインスタンスを多数作成するContractFactoryによって作成されるようなContractを低いガス代で量産するのに便利ですとOpenZeppelinには記載されてます。
Clonesは指定したContractの複製(クローン)を低いガス代で作るための関数(clone)を用意したlibraryなのです。

🌝 コード解説

実際のコードを見つつ、Clonesの仕組みをとらえていきます。

library Clones {
    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
     *
     * This function uses the create opcode, which should never revert.
     */
    function clone(address implementation) internal returns (address instance) {
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
            mstore(add(ptr, 0x14), shl(0x60, implementation))
            mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
            instance := create(0, ptr, 0x37)
        }
        require(instance != address(0), "ERC1167: create failed");
    }
}

基本的には関数cloneだけ抑えておけば大丈夫です。clone(address implementation)はimplementationで指定したコントラクトのコピーを作成します。一行ずつ確認していきます。

引数のaddress implementationはコピー元のContractのアドレスです。
let ptr := mload(0x40) によって、ptrに自由に使えるメモリのポインタ(free memory pointer)を格納しています。free memory pointerは常にメモリの0x40に入っています。(Solidity Document)

mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)によってptrに謎の数値を保存しています。この謎の値、0x3d602d80600a3d3981f3363d3d373d3d3d363d73については後ほど解説します。
mstore(add(ptr, 0x14), shl(0x60, implementation)) ではptr+20番目のアドレスにimplementationを格納しています。shlは左シフトで、shl(0x60, implementation)によってimplementationを左に12bytesシフトしています。(solidityは1ワード=32bytesなので、32-20 =12bytes分左にずらします。20はimplementationのbytes数です。)
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)も同様に0x5af43d82803e903d91602b57fd5bf3という値をptr+28番目のアドレスに格納しています。
以上で、ptrアドレスからの45bytesの領域には3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3という値が入っています。bebebebebebebebebebebebebebebebebebebebeはimplementationの値を指します。

そして、instance := create(0, ptr, 0x37)についてです。create(v:u256, p:u256, n:u256) は mem[p..(p+n))のコードで構成されるContractを作り、ついでにv wei送り、作成されたContractのアドレスを返します。(Solidity Document) つまりptrアドレスから45bytesのメモリ領域に格納されたコードのContractを作り、できたContractのアドレスを変数instanceに保存するというのがこのコードの意味です。
ではptrアドレスから45bytesの領域に格納された謎の値を解読していきます。
まず、初めの10bytes 3d602d80600a3d3981f3 は作成するコピーのContractのconstructorをコンパイルしたものです。decompilerでデコンパイルしてみると以下のコードが出てきます。

function constructor() {
        var var0 = returndata.length;
        memory[returndata.length:returndata.length + 0x2d] = code[0x0a:0x37];
        return memory[var0:var0 + 0x2d];
    }

このconstructorではcode領域の10bytes目から44bytes目までをmemory領域に保存して返します。つまり、363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3をmemory領域に保存するのです。(returndataとかcodeとかmemoryとかいうわけわかんない変数が出てきましたが、この記事が参考になります。)

では363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3decompilerでデコンパイルしてみます。以下のコードが出てきました。

contract Contract {
    function main() {
        var temp0 = msg.data.length;
        memory[returndata.length:returndata.length + temp0] = msg.data[returndata.length:returndata.length + temp0];
        var temp1 = returndata.length;
        var temp2;
        temp2, memory[returndata.length:returndata.length + returndata.length] = address(0xbebebebebebebebebebebebebebebebebebebebe).delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length]);
        var temp3 = returndata.length;
        memory[temp1:temp1 + temp3] = returndata[temp1:temp1 + temp3];
        var var1 = temp1;
        var var0 = returndata.length;
    
        if (temp2) { return memory[var1:var1 + var0]; }
        else { revert(memory[var1:var1 + var0]); }
    }
}

この関数を簡単にまとめると、コピー元のimplementation Contractの指定した関数をdelegatecallするというものです。delegatecallですから、コピー元のimplementation Contractではなく、コピー先のClone Contractのストレージ領域を書き換えます。つまりClone Contractは自身のcode領域にimplementation Contractのコードをコピーすることなくimplementation Contractで定義した関数を実行し、自身のストレージ領域を変化させることができます。このようにして、Clone Contractはimplementation Contractを擬似的にコピーし、Clone Contract生成時のガス代を抑えているのです。

🌅 終わり

Clones libraryは、Contractを量産するContractでは結構採用されていてChocofactoryでも使われています。

書き忘れていましたが、Clone ContractはEIP-1167で基準が提案されています。
Clone Contractはコピー元のコードを持っていないので、Etherscanでverificationできるのか疑問ですが、できる的なことをEtherscan community supportの方が言っています。(参考)
というか、EIP1167はそのためのルールを定めたものです。

以上、Clonesの解説でした。solidityを触り始めた頃はdelegatecallの使い道がわかっていませんでしたが、よく考えられたオペコードだなあと今では感心しています。

ご拝読ありがとうございました。

🙇 終わり

Discussion