Solidityを用いてアップグレード可能なコントラクトを作成する
初めに
Ethereumの開発では、コントラクトはデプロイしたのちに修正することができないという課題があります。 本記事では、Zeppelin_osの実装方法から、アップグレード可能なコントラクトをどのように作成するかを整理します。
2つのkey point
- コントラクトのあるファンクションがコールされ、そのファンクションがそのコントラクトに定義されていない場合、fallback関数が呼ばれます。proxyコントラクトはこの機能を利用して、カスタムフォールバックを作成します。
- コントラクトAがコントラクトBにコールをdelegateしたとしても、その実行は、コントラクトAの状況下で行われます。これは、msg.value、msg.senderはコントラクトAの段階でキープされ、ストレージ領域はコントラクトAに保存されることを意味します。
本記事は、現在Zeppelin_osが採用をしようとしているupgradeability-using-unstructured-storageパターンについて、解説したメモです。
このパターンの構造は以下のようになっています。
Proxyコントラクト
まず、ユーザーはプロキシコントラクトを介してロジックコントラクトにアクセスします。 そして、ストレージ領域はプロキシコントラクトに保存されます。 そのため、何度アップグレードしても、保存される領域は変わりません。
/** * @title Proxy * @dev Gives the possibility to delegate any call to a foreign implementation. */ contract Proxy { /** * @dev Tells the address of the implementation where every call will be delegated. * @return address of the implementation to which it will be delegated */ function implementation() public view returns (address); /** * @dev Fallback function allowing to perform a delegatecall to the given implementation. * This function will return whatever the implementation call returns */ function () payable public { // ロジックコントラクトのアドレス address _impl = implementation(); require(_impl != address(0)); assembly { // 0x40は、特別なメモリ領域で、次に使えるフリーのメモリーポインターを格納している。 let ptr := mload(0x40) // ptr(フリーポインター)のメモリー領域に、calldataの0からcalldatasize分までをコピーする calldatacopy(ptr, 0, calldatasize) /* gas: delegatecallに用いるgas量 _impl: ロジックコントラクトのアドレス ptr: calldatacopyでコピーしたメモリーのポインター calldatasize: calldatasize 0(outdata): delegatecallが実行された後に返却されるデータ、今回はまだ実行していないため、0 0(outsize): delegatecallが実行された後に返却されるデータのサイズ、今回はまだ実行していないため、0 */ let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0) // returndataのサイズ let size := returndatasize // メモリーのフリーポインターに0からreturndata分までコピーする returndatacopy(ptr, 0, size) // 結果を返却する switch result // 0だった場合、エラーのためrevert case 0 { revert(ptr, size) } // それ以外はデータを返す default { return(ptr, size) } } } }
UpgradeabilityProxyコントラクト
このコントラクトは、delegateするコントラクトの情報をアップデートする情報を保有します。
implementationPosition
に、ロジックコントラクトのアドレスを保持しておきます。
また、このポジションにアクセスする際には、アセンブリを用いています。
/** * @title UpgradeabilityProxy * @dev This contract represents a proxy where the implementation address to which it will delegate can be upgraded */ contract UpgradeabilityProxy is Proxy { /** * @dev This event will be emitted every time the implementation gets upgraded * @param implementation representing the address of the upgraded implementation */ event Upgraded(address indexed implementation); // Storage position of the address of the current implementation bytes32 private constant implementationPosition = keccak256("org.zeppelinos.proxy.implementation"); /** * @dev Constructor function */ function UpgradeabilityProxy() public {} /** * @dev Tells the address of the current implementation * @return address of the current implementation */ function implementation() public view returns (address impl) { bytes32 position = implementationPosition; assembly { impl := sload(position) } } /** * @dev Sets the address of the current implementation * @param newImplementation address representing the new implementation to be set */ function setImplementation(address newImplementation) internal { bytes32 position = implementationPosition; assembly { sstore(position, newImplementation) } } /** * @dev Upgrades the implementation address * @param newImplementation representing the address of the new implementation to be set */ function _upgradeTo(address newImplementation) internal { address currentImplementation = implementation(); require(currentImplementation != newImplementation); setImplementation(newImplementation); emit Upgraded(newImplementation); } }
OwnedUpgradeabilityProxyコントラクト
このコントラクトは、プロキシコントラクトのowner情報にアクセスする情報を保有しています。
/** * @title OwnedUpgradeabilityProxy * @dev This contract combines an upgradeability proxy with basic authorization control functionalities */ contract OwnedUpgradeabilityProxy is UpgradeabilityProxy { /** * @dev Event to show ownership has been transferred * @param previousOwner representing the address of the previous owner * @param newOwner representing the address of the new owner */ event ProxyOwnershipTransferred(address previousOwner, address newOwner); // Storage position of the owner of the contract bytes32 private constant proxyOwnerPosition = keccak256("org.zeppelinos.proxy.owner"); /** * @dev the constructor sets the original owner of the contract to the sender account. */ function OwnedUpgradeabilityProxy() public { setUpgradeabilityOwner(msg.sender); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyProxyOwner() { require(msg.sender == proxyOwner()); _; } /** * @dev Tells the address of the owner * @return the address of the owner */ function proxyOwner() public view returns (address owner) { bytes32 position = proxyOwnerPosition; assembly { owner := sload(position) } } /** * @dev Sets the address of the owner */ function setUpgradeabilityOwner(address newProxyOwner) internal { bytes32 position = proxyOwnerPosition; assembly { sstore(position, newProxyOwner) } } /** * @dev Allows the current owner to transfer control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function transferProxyOwnership(address newOwner) public onlyProxyOwner { require(newOwner != address(0)); emit ProxyOwnershipTransferred(proxyOwner(), newOwner); setUpgradeabilityOwner(newOwner); } /** * @dev Allows the proxy owner to upgrade the current version of the proxy. * @param implementation representing the address of the new implementation to be set. */ function upgradeTo(address implementation) public onlyProxyOwner { _upgradeTo(implementation); } /** * @dev Allows the proxy owner to upgrade the current version of the proxy and call the new implementation * to initialize whatever is needed through a low level call. * @param implementation representing the address of the new implementation to be set. * @param data represents the msg.data to bet sent in the low level call. This parameter may include the function * signature of the implementation to be called with the needed payload */ function upgradeToAndCall(address implementation, bytes data) payable public onlyProxyOwner { upgradeTo(implementation); require(this.call.value(msg.value)(data)); } }
注意事項
このパターンでは、全ての情報はプロキシコントラクトに保存されています。そのため、Ownableなどでコンストラクターを用いて情報を保存している場合、独自にコンストラクターで初期化する関数を実装しなければなりません。
そして、その関数をupgradeToAndCall
関数を呼び出して、実行します。
bool internal _initialized; modifier onlyOnce() { require(!_initialized); _; } function initialize(address _owner) public onlyOnce { require(_owner != address(0)); owner = _owner; _initialized = true; }
サンプルとしてテストコードを提示します。
const encodeCall = require('./helpers/encodeCall'); const ExampleToken = artifacts.require("ExampleToken"); const OwnedUpgradeabilityProxy = artifacts.require("OwnedUpgradeabilityProxy"); const owner = web3.eth.accounts[0]; const initializeData = encodeCall('initialize', ['address'], [owner]) const initializeData1 = encodeCall('transfer', ['address','uint256'], [owner,1000]) let exampleToken; let exampleTokenBehavior; let ownedUpgradeabilityProxy; contract("ExampleToken", (accounts) => { it("deploy new ExampleToken", async () => { ownedUpgradeabilityProxy = await OwnedUpgradeabilityProxy.new({from:owner}); exampleTokenBehavior = await ExampleToken.new({from:owner}); // exampleTokenBehaviorを紐付けるとともに、initialize関数を呼び出す。 await ownedUpgradeabilityProxy.upgradeToAndCall(exampleTokenBehavior.address, initializeData, {from:owner}); exampleToken = await ExampleToken.at(ownedUpgradeabilityProxy.address); await exampleToken.mint(accounts[1], 1000, {from:accounts[0]}); let balance = await exampleToken.balanceOf(accounts[1]); assert.equal(1000, balance, "balance is not correct"); }); })
参考資料