ZeppelinOS Proxyの実装
TL;DR
Zeppelin OSのProxyコントラクトがどういう実装になっているかのコードリーディング。
genericなproxyを作るためにはtarget contract(実際に実行したいコントラクト)に対して、senderからproxyに送られてきたcalldataを透過的に扱えるようにする必要がある。
そのためにfallback関数を使って任意のcalldataをハンドリング出来るようにし、assemblyを使ってcalldataを取得してdelegatecallを行い、結果をsenderに返却するようになっている。
引用元のコード
code: Proxy.sol
pragma solidity ^0.4.24;
/**
* @title Proxy
* @dev Implements delegation of calls to other contracts, with proper
* forwarding of return values and bubbling of failures.
* It defines a fallback function that delegates all calls to the address
* returned by the abstract _implementation() internal function.
*/
contract Proxy {
/**
* @dev Fallback function.
* Implemented entirely in _fallback.
*/
function () payable external {
_fallback();
}
/**
* @return The Address of the implementation.
*/
function _implementation() internal view returns (address);
/**
* @dev Delegates execution to an implementation contract.
* This is a low level function that doesn't return to its internal call site.
* It will return to the external caller whatever the implementation returns.
* @param implementation Address to delegate.
*/
function _delegate(address implementation) internal {
assembly {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 0, calldatasize)
// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
let result := delegatecall(gas, implementation, 0, calldatasize, 0, 0)
// Copy the returned data.
returndatacopy(0, 0, returndatasize)
switch result
// delegatecall returns 0 on error.
case 0 { revert(0, returndatasize) }
default { return(0, returndatasize) }
}
}
/**
* @dev Function that is run as the first thing in the fallback function.
* Can be redefined in derived contracts to add functionality.
* Redefinitions must call super._willFallback().
*/
function _willFallback() internal {
}
/**
* @dev fallback implementation.
* Extracted to enable manual triggering.
*/
function _fallback() internal {
_willFallback();
_delegate(_implementation());
}
}
詳細
L15-17
code: fallback function
function () payable external {
_fallback();
}
genericなProxyとして使えるようにするには任意の(正常な)calldataを受け取れる必要があるため、fallback関数を利用して受け取れるようにしている
L35
code: calldatacopy
calldatacopy(0, 0, calldatasize)
calldataをメモリ0の位置にコピーする
calldatacopy(t, f, s)
calldataのfの位置からsバイトをtの位置へコピーする
calldatasize
calldataのサイズ
calldata
どの関数をどういった引数で呼び出すかを決めるためのdata
t = 0
Solidityの仕様ではメモリ0x00~0x7fは内部的に使われる領域 だが、少なくとも現時点でのコンパイラ(solc)の実装では、fallback関数内かつ _delegate 関数全体が1つのassemblyで完結しているという条件では、assembly内ではメモリの操作が行われないため上書きして良い(らしい) 安全のためにfree memory pointerが指している位置にコピーすることも考えられるが、t=0にするよりはgasがかかる
L39
code: delegatecall
let result := delegatecall(gas, implementation, 0, calldatasize, 0, 0)
実際にdelegatecallをするところ
delegatecall(g, a, in, insize, out, outsize)
メモリ $ [in...(in+insize)) のinputとg gasを与えてcallerとcallvalueは保持したままaのaddressのコントラクトをcallし、メモリ $ [out...(out+outsize)) の位置に結果を受け取る
gas
使える残りのgas
implementation
delegatecallしたいコントラクトのアドレス
0, calldatasize
L35でメモリにロードしたcalldataを使う
0, 0
outputのsizeが固定ではないので0にしておく
別の方法でoutputを取得するということ
result
delegatecallが成功すれば1, (out of gasなどで)失敗すれば0になる
L42
code: returndatacopy
returndatacopy(0, 0, returndatasize)
returnされたoutputをメモリに保存する
returndatacopy(t, f, s)
returndataのfの位置からsバイトをメモリtの位置にコピーする
returndatasize
returndataのサイズ
どのコントラクトに何のcalldataでdelegatecallするかで動的に変わるのでここで取得している
L46
code: revert
case 0 { revert(0, returndatasize) }
delegatecall失敗時にreturnされたdataをそのまま使ってrevertする
L47
code: return
default { return(0, returndatasize) }
delegatecall成功時にreturnされたdataをそのまま使って通常終了する
参考
assemblyの各opcodeの仕様