|
1 |
| -# はじめに |
| 1 | +<!-- # はじめに |
2 | 2 |
|
3 | 3 | Upgradeableなコントラクト、スマートコントラクトを書き始めた人なら一度は聞いたことのある用語だと思います。Upgradeableなコントラクトについては、OpenZeppelinのLibraryも充実しています。そのため、すでに多くの人がUpgradeableなコントラクトを書いたことがあるかもしれません。
|
4 | 4 |
|
5 |
| -今回のブログでは、なぜコントラクトがUpgradeable可能になるかについて書いていきます。 DELEGATECALL、Proxy patternを理解している人には退屈な内容だと思います。それでは見ていきます。 |
| 5 | +今回のブログでは、なぜコントラクトがUpgradeable可能になるかについて書いていきます。 delegatecall, proxy patternを理解している人には退屈な内容になると思います。 |
6 | 6 |
|
7 |
| -# なぜUpgradeableなコントラクトが必要とされるのか? |
| 7 | +
|
| 8 | +# Upgradeableなコントラクトの必要性 |
8 | 9 |
|
9 | 10 | スマートコントラクトはアプリケーション開発において、バックエンドを担うとされます。しかし、本来のバックエンド開発と大きな違いがあります。それは、一度デプロイされたスマートコントラクトは変更ができない点です。このことはアプリケーション開発を難しくします。デプロイ後、変更のできないスマートコントラクトでは、バグの修正、新規機能追加などができません。その解決策とされるのが、Upgradeableなコントラクトです。
|
10 | 11 |
|
| 12 | +
|
11 | 13 | # なぜUpgradeableなコントラクトが可能になるのか?
|
12 | 14 |
|
13 |
| -スマートコントラクトは、デプロイ後変更できない。それではなぜUpgradeableなコントラクトは可能なのでしょうか?Upgradeableなコントラクトが可能になる背景には、DelegatecallとProxy patternがあります。 |
| 15 | +スマートコントラクトは、デプロイ後変更できない。それではなぜUpgradeableなコントラクトが可能になるのでしょうか?Upgradeableなコントラクトが可能になる背景には、DelegatecallとProxy patternがあります。 |
14 | 16 |
|
15 | 17 | ## Delegatecall
|
16 | 18 |
|
| 19 | +delegatecallはLow-level functionで、あるコントラクトから別のコントラクトを呼ぶ際に使われます。delegatecallには、二つの特徴があります。コントラクトAからコントラクトBを呼び出すというケースを想定します。一つ目の特徴は、コントラクトBのコードはロジックとして利用されます。そして、コントラクトAのstate variablesを更新します。二つ目の特徴は、msg.senderがコントラクトAを呼び出したアドレスになるということです。(delegatecallを使わずコントラクトAからコントラクトBを呼ぶ場合。この時、コントラクトBで実行されるコードは、コントラクトBのState variablesを参照し更新します。msg.senderはコントラクトAなります。) |
| 20 | +
|
| 21 | +文章だけでは理解するのが大変だと思います。具体例をみていきます。以下のコードは、[Solidity by Example](https://solidity-by-example.org/delegatecall)からの引用です。 |
| 22 | +
|
| 23 | +``` |
| 24 | +// SPDX-License-Identifier: MIT |
| 25 | +pragma solidity ^0.8.13; |
| 26 | +
|
| 27 | +// NOTE: Deploy this contract first |
| 28 | +contract B { |
| 29 | + // NOTE: storage layout must be the same as contract A |
| 30 | + uint public num; |
| 31 | + address public sender; |
| 32 | + uint public value; |
| 33 | +
|
| 34 | + function setVars(uint _num) public payable { |
| 35 | + num = _num; |
| 36 | + sender = msg.sender; |
| 37 | + value = msg.value; |
| 38 | + } |
| 39 | +} |
| 40 | +
|
| 41 | +contract A { |
| 42 | + uint public num; |
| 43 | + address public sender; |
| 44 | + uint public value; |
| 45 | +
|
| 46 | + function setVars(address _contract, uint _num) public payable { |
| 47 | + // A's storage is set, B is not modified. |
| 48 | + (bool success, bytes memory data) = _contract.delegatecall( |
| 49 | + abi.encodeWithSignature("setVars(uint256)", _num) |
| 50 | + ); |
| 51 | + } |
| 52 | +} |
| 53 | +``` |
| 54 | +
|
| 55 | +コントラクトAのsetVarsという関数から、コントラクトBのsetVarsという関数を呼び出しています。コントラクトAのsetVarsがパラメーターとして、コントラクトBのアドレスを取ります。そして、_contract.delegatecall()という部分でdelegatecallが使われて、コントラクトBのsetVars関数が呼ばれています。コントラクトBのsetVars関数の中で、num、sender、valueというstate variablesが更新されます。実際に更新されるstate variablesは、コントラクトAのstate variablesになります。senderにはコントラクトAのsetVars関数を呼び出したアドレスが代入され、valueはコントラクトAが呼び出された際に、送金されたEtherになります。このことは、以下のテストコードでも確認することができます。興味がある方は、テストコードをhardhatで実行してみてください。 |
| 56 | +
|
| 57 | +``` |
| 58 | +import { ethers } from 'hardhat' |
| 59 | +import { expect } from 'chai' |
| 60 | +import { Contract, constants } from 'ethers' |
| 61 | +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
| 62 | +
|
| 63 | +describe("Delegatecall", function () { |
| 64 | + let constractA: Contract |
| 65 | + let contractB: Contract |
| 66 | + let user1: SignerWithAddress |
| 67 | +
|
| 68 | + before(async () => { |
| 69 | + [user1] = await ethers.getSigners() |
| 70 | + contractB = await (await ethers.getContractFactory("B")).deploy() |
| 71 | + constractA = await (await ethers.getContractFactory("A")).deploy() |
| 72 | + }) |
| 73 | +
|
| 74 | + it("prove of the delegatecall", async () => { |
| 75 | + expect(await constractA.num()).to.equal(0) |
| 76 | + expect(await constractA.sender()).to.equal(constants.AddressZero) |
| 77 | + expect(await constractA.value()).to.equal(0) |
| 78 | + |
| 79 | + const options = {value: ethers.utils.parseEther("1.0")} |
| 80 | + |
| 81 | + await constractA.connect(user1).setVars(contractB.address, 7, options) |
17 | 82 |
|
| 83 | + expect(await constractA.num()).to.equal(7) |
| 84 | + expect(await constractA.sender()).to.equal(user1.address) |
| 85 | + expect(await constractA.value()).to.equal(options.value) |
| 86 | + }) |
| 87 | +}); |
| 88 | +``` |
| 89 | +
|
| 90 | +以上がdelegatecallの説明になります。まとめると、 |
| 91 | +
|
| 92 | +①コントラクトBはロジックを実行するコードとなり、コントラクトA(呼び出し元)のstate variablesが更新されること |
| 93 | +②コントラクトBのmsg.senderはコントラクトAではなく、コントラクトAを呼び出したアドレスになること |
| 94 | +
|
| 95 | +の二点が重要です。 |
18 | 96 |
|
19 | 97 | ## Proxy pattern
|
20 | 98 |
|
| 99 | +Proxy patternでは、ロジックとなるコントラクトのアドレスをstate variableとして保存します。さらに呼び出し元(コントラクトA)では、fallback functionを使用します。このfallback functionの中でdelegatecallが使われ、保存したアドレスをロジックコントラクトとして利用することを可能にします。 |
| 100 | +
|
| 101 | +このロジックコントラクト自体は、他のコントラクトと同様、変更できないコントラクトです。コントラクトをUpgradeableにするのは、先ほど保存したロジックコントラクトのアドレスを更新します(例えば、コントラクトBアドレスから、コントラクトCのアドレスに変更する)これにより、delegatecallの対象はコントラクトCになります。delegatecallの特徴から、今までコントラクトBのロジックを利用して更新されてきたコントラクトAのstate variablesは維持されることになります。 |
| 102 | +
|
| 103 | +# 最後に |
| 104 | +
|
| 105 | +今回のブログでは、なぜUpgradeableなコントラクトが可能になるのかについてまとめました。デプロイ後、変更できないはずのスマートコントラクトが、なぜUpgradeableになるのか理解できたと思います。次回のブログでは、Upgradeableなコントラクトを作成する際の注意点について書きたいと思います。最後まで読んでいただき、ありがとうございました。 |
| 106 | +
|
| 107 | +# 参考資料 |
| 108 | +
|
| 109 | +Solidity<br /> |
| 110 | +https://docs.soliditylang.org/en/v0.8.15/index.html |
| 111 | +
|
| 112 | +The State of Smart Contract Upgrades <br /> |
| 113 | +https://blog.openzeppelin.com/the-state-of-smart-contract-upgrades/#upgrade-patterns |
| 114 | +
|
| 115 | +Proxy Upgrade Pattern <br /> |
| 116 | +https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies --> |
| 117 | + |
| 118 | + |
| 119 | + |
21 | 120 |
|
0 commit comments