自己紹介
こんにちは!ブロックチェーンコースに所属しているMassunです.
DeFiプロダクトを調べたりしています.UniswapV3が5/5にローンチすることが発表されとても楽しみです!!
概要
dYdX,PoolTogether,USDCなどの数多くのプロジェクトで使われている,デリゲートのデザインパターンである Transparent proxy patternに焦点を当ててまとめます.
なぜスマートコントラクトをアップグレーダブルにするのか
スマートコントラクトはイミュータブルで一度デプロイしたコードは変更できません.イミュータブルな性質がブロックチェーン,スマートコントラクトの利点でもありますが,逆に言うと,バグの修正やプロダクトを改良などをしてコードをバージョンアップをすることができません.
そこで,アップグレーダブルにするために,ユーザーと直接インタラクトして,ロジックを担うコントラクトへ仲介するコントラクトを作って,参照先のロジックを変えることでアップグレーダブルにするというデザインパターンを使います.これがプロキシパターンです.
プロキシコントラクトの参照先を変えることは,アドミン権限だけができるようにします.
アップグレーダブルなコントラクトを作るときの実装コントラクトの実装の注意についてはここでは深くは踏み込まずに次に進みます.
なぜTransparent Proxy Patternを使うのか
一番ベーシックなプロキシの方法はプロキシコントラクトは受け取った全てのコールをロジックのコントラクトにデリゲートする方法ですが,これには問題があります.この方法では,プロキシコントラクト内の関数の関数セレクタ(関数を判別する識別子)と実装コントラクト内の関数のセレクタが衝突(slector clashing)していたとき,攻撃に利用される可能性があります. どのようにして悪用されるかは Nomic Labsの記事を参考にしてください....
関数セレクタは,関数名(引数の型) (例えばupgradeTo(address)
)をハッシュした最初のたった4バイトなので,異なる関数名でも同じ関数セレクタになることは全然ありえます.
コンパイラは同じコントラクト内の関数ではセレクタが衝突していないかチェックするのですが,複数のコントラクト間で衝突していないかは確認しません.
解決策
- Admin以外のアカウントがプロキシコントラクトを呼んだ時,たとえプロキシコントラクトにあるアドミンの関数にセレクタが一致していたとしてもロジックコントラクトに全てデリゲートする.
- Adminがプロキシコントラクトを呼んだときは,プロキシ内の関数のみしか実行できず,どんな呼び出しもロジックコントラクトにデリゲートしない.
このように,msg.sender
がadmin
か否かで場合分けします.OpenZeppelinのProxy
のライブラリは,
function _fallback() internal {
_delegate();
}
modifier ifAdmin() {
if (msg.sender == _admin()) {
_; // adminならば,何にもせずそのまま実行フローを元の関数に返す.
} else {
_fallback(); // admin以外なら,全てdelegateして終わる.
}
}
このifAdmin
modifierをdelegateする関数以外の全ての関数に付けています.
アップグレードする時の流れ
これでOKと思うのですが,アドミンのアドレスで実装コントラクトの関数を実行したい場合,それができなくなります.そのため,EOAのアドミンの代わりに,実装コントラクトを変える関数upgradeTo(implementation)
を呼ぶProxyAdmin
というコントラクトにアップグレードの権限を移します.こうすることでアップグレードに関することだけを行うProxyAdmin
がProxy
のインターフェースとなり,アドミンを別のアドミンにしたい時はProxyAdmin
で権限を移すことになります.
要するに,Proxyの実際のadminがProxyAdmin
ということです.通常,ProxyAdmin
はネットワーク(mainet,ropsten...)に一つデプロイすればいいです.
アップグレードするときは,
- 新しい実装コントラクトをデプロイする
ProxyAdmin
のowner
がProxyAdmin
のupgrade(_proxy,_impl)
を呼ぶ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;
}
}
次に,TransparentProxy
やProxyAdmin
のコントラクトが必要ですが,実は,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
を実行してリセットを試してください.
終わり
スマートコントラクトをアップグレーダブルにする設計がわかりました.主にパブリックチェーンのプロジェクトを追っているので何か共有できたらいいですね!