💎🎉 OpenZeppelin Clones 解説 🎉💎
🌞 概要
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とかいうわけわかんない変数が出てきましたが、この記事が参考になります。)
では363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3
をdecompilerでデコンパイルしてみます。以下のコードが出てきました。
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