Transparent Proxy Patternでスマートコントラクトをアップグレーダブルにする

dYdX,PoolTogether,USDCなどの数多くのプロジェクトで使われている,デリゲートのデザインパターンである Transparent proxy patternに焦点を当ててまとめます.

Transparent Proxy Patternでスマートコントラクトをアップグレーダブルにする

自己紹介

こんにちは!ブロックチェーンコースに所属しているMassunです.
DeFiプロダクトを調べたりしています.UniswapV3が5/5にローンチすることが発表されとても楽しみです!!

概要

dYdX,PoolTogether,USDCなどの数多くのプロジェクトで使われている,デリゲートのデザインパターンである Transparent proxy patternに焦点を当ててまとめます.

なぜスマートコントラクトをアップグレーダブルにするのか

スマートコントラクトはイミュータブルで一度デプロイしたコードは変更できません.イミュータブルな性質がブロックチェーン,スマートコントラクトの利点でもありますが,逆に言うと,バグの修正やプロダクトを改良などをしてコードをバージョンアップをすることができません.

そこで,アップグレーダブルにするために,ユーザーと直接インタラクトして,ロジックを担うコントラクトへ仲介するコントラクトを作って,参照先のロジックを変えることでアップグレーダブルにするというデザインパターンを使います.これがプロキシパターンです.

プロキシコントラクトの参照先を変えることは,アドミン権限だけができるようにします.

アップグレーダブルなコントラクトを作るときの実装コントラクトの実装の注意についてはここでは深くは踏み込まずに次に進みます.


なぜTransparent Proxy Patternを使うのか

一番ベーシックなプロキシの方法はプロキシコントラクトは受け取った全てのコールをロジックのコントラクトにデリゲートする方法ですが,これには問題があります.この方法では,プロキシコントラクト内の関数の関数セレクタ(関数を判別する識別子)と実装コントラクト内の関数のセレクタが衝突(slector clashing)していたとき,攻撃に利用される可能性があります. どのようにして悪用されるかは Nomic Labsの記事を参考にしてください....

関数セレクタは,関数名(引数の型) (例えば  upgradeTo(address))をハッシュした最初のたった4バイトなので,異なる関数名でも同じ関数セレクタになることは全然ありえます.
コンパイラは同じコントラクト内の関数ではセレクタが衝突していないかチェックするのですが,複数のコントラクト間で衝突していないかは確認しません.

解決策

  • Admin以外のアカウントがプロキシコントラクトを呼んだ時,たとえプロキシコントラクトにあるアドミンの関数にセレクタが一致していたとしてもロジックコントラクトに全てデリゲートする.
  • Adminがプロキシコントラクトを呼んだときは,プロキシ内の関数のみしか実行できず,どんな呼び出しもロジックコントラクトにデリゲートしない.

このように,msg.senderadminか否かで場合分けします.OpenZeppelinのProxyのライブラリは,

    function _fallback() internal {
        _delegate();
    }

    modifier ifAdmin() {
        if (msg.sender == _admin()) {
            _; // adminならば,何にもせずそのまま実行フローを元の関数に返す.
        } else {
            _fallback(); // admin以外なら,全てdelegateして終わる.
        }
    }

このifAdminmodifierをdelegateする関数以外の全ての関数に付けています.

アップグレードする時の流れ

これでOKと思うのですが,アドミンのアドレスで実装コントラクトの関数を実行したい場合,それができなくなります.そのため,EOAのアドミンの代わりに,実装コントラクトを変える関数upgradeTo(implementation)を呼ぶProxyAdminというコントラクトにアップグレードの権限を移します.こうすることでアップグレードに関することだけを行うProxyAdminProxyのインターフェースとなり,アドミンを別のアドミンにしたい時はProxyAdminで権限を移すことになります.

要するに,Proxyの実際のadminがProxyAdminということです.通常,ProxyAdminはネットワーク(mainet,ropsten...)に一つデプロイすればいいです.
アップグレードするときは,

  1. 新しい実装コントラクトをデプロイする
  2. ProxyAdminownerProxyAdminupgrade(_proxy,_impl)を呼ぶ
  3. ProxyAdminを通してProxyコントラクトの参照先が新しい実装コントラクトになる.
    という手順を踏みます.

実際に使う

ここまで,Transparent proxy patternについて説明しましたが,実際に使ってみましょう.
ソースコードはこちらです

ターミナルで必要なモジュールをインストールします.
yarn

次に,実装コントラクトとして,valueを保存し書き換えるLogcV1コントラクトをcontracts/LogicV1.solに作りましょう.

まず,Initializableコントラクトを継承して,変数valueを宣言だけします.(実装コントラクトはステートの宣言時に値を割り当ててはいけません)

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import ./interfaces/ILogic.sol";
contract LogicV1 is Initializable, ILogic {
    uint256 public value;
}

次に,実装コントラクトはconstructorが使えないので,代わりにinitializeなどの名前にした一度だけ実行する関数を用意します.initiazlier修飾子を付けて一度だけ実行されるようにします.

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
・・・・
function initialize(uint256 _value) public override initializer {
        value = _value;
}
・・・

次に,valueをセットする関数を作ります

    function setValue(uint256 _value, uint256 num) 
        public
        override
        returns (uint256) 
    {
        value = _value + num;
        return value;
    }

最終的にこうなります.

pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "./interfaces/ILogic.sol";

contract LogicV1 is Initializable, ILogic {
    uint256 public value;

    function initialize(uint256 _value) public override initializer {
        value = _value;
    }

    function setValue(uint256 _value, uint256 num) 
        public
        override 
        returns (uint256)
    {
        value = _value + num;
        return value;
    }
}

次に,contracts/LogicV2.solにアップグレードしたLogicV2を作ってみました,
LogicV2のアップグレードで,setValueの計算の仕方を変更しています.

pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "./interfaces/ILogic.sol";

contract LogicV2 is Initializable, ILogic {
    uint256 public value;

    function initialize(uint256 _value)
        public
        override
        initializer
    {
        value = _value;
    }

    function setValue(uint256 _value, uint256 num) 
        public
        override
        returns (uint256)
    {
        value = 2 * _value + num;
        return value;
    }

}

次に,TransparentProxyProxyAdminのコントラクトが必要ですが,実は,OpenZeppelinがこれらのライブラリを提供していて,しかも内部でラップしてアップグレードが可能かどうかチェックしてくれるプラグインを提供しているのでそれを使いましょう.

正しくアップグレードできているかテストしてみます.
test/test.tsに次のコードを作ります.

import { expect } from "chai";
import { ethers, upgrades } from 'hardhat';
const toWei = ethers.utils.parseEther


describe("Logic", function () {
    it('test upgrade', async () => {
        const Logic = await ethers.getContractFactory("LogicV1");
        const LogicV2 = await ethers.getContractFactory("LogicV2");
        // deploy
        const instance = await upgrades.deployProxy(
            Logic, [toWei("100")]
        );
            
        // 初期値100
        expect((await instance.value())).to.equal(toWei("100"));
        
        // LogicV1は150+10=160をセットする
        await instance.setValue(toWei("150"), toWei("10"));
        expect((await instance.value())).to.equal(toWei("160"));

        // upgrade
        const upgraded = await upgrades.upgradeProxy(
            instance.address, 
            LogicV2
        );
        
        // LogicV2は2*200+10=410をセットする
        await upgraded.setValue(toWei("200"), toWei("10"));
        expect((await upgraded.value())).to.equal(toWei("410"));
    });
});

ターミナルでローカルにチェーンを立ち上げます.
npx hardhat node --network hardhat

別のターミナルで次のコマンドを実行してテストを実行します.
npx hardhat test test/test.ts --network localhost

テストが次のようにpassするはずです.setValue関数が正しくアップグレードできたことが確認できました.

・・・
Creating Typechain artifacts in directory typechain for target ethers-v5
Successfully generated Typechain artifacts!


  Logic
    ✓ test upgrade (1519ms)


  1 passing (2s)

InvalidDeployment [Error]: Invalid deployment with address 0x3.. and txHash 0xc.. エラーが出るときは
アップグレードした実装コントラクトの情報は.openzeppelin/<network>.jsonに保存されているので
rm -rf ./openzeppelin
rm -rf artifacts
を実行してリセットを試してください.

終わり

スマートコントラクトをアップグレーダブルにする設計がわかりました.主にパブリックチェーンのプロジェクトを追っているので何か共有できたらいいですね!

参考

The transparent proxy pattern
Much has been discussed around proxy patterns and how to best achieve upgradeability in Ethereum smart contracts. The underlying idea is quite simple: instead of interacting with your smart contrac…
Upgrades Plugins - OpenZeppelin Docs
Proxies - OpenZeppelin Docs