【Solidity】Proxyコントラクトの技術要素

# はじめに（Introduction）

EVM互換ブロックチェーンのコントラクトにおいて、最近ではアップグレード可能なコントラクトにする傾向があります。
アップグレードは、Proxyコントラクトを用いて行うことが大半です。
そこで、Proxyコントラクトの主な技術要素をまとめてみます。

# Proxy

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770659/630ebdea-984b-725b-219d-761d57854076.png)

Proxyコントラクトの動きは上図のイメージとなります。

手順としては、Proxyコントラクトから機能を呼び出します。
Proxyコントラクトの`fallback`関数から`delegatecall`を使い、実装（Implementation）コントラクトを呼び出します。
データはProxyコントラクトの`Storage`に格納されます。

したがって、`fallback`、`delegatecall`、`Storage`が主な技術要素となります。

また、アップグレードは、`delegatecall`の対象を新しい実装（Implementation）コントラクトに変更することで実現します。

# 主な技術要素

`fallback`、`Storage`、`delegatecall` について見てみます。

## fallback

例えば、以下のコントラクトに対して`a()`機能の呼び出しは可能ですが、実装されていない`b()`や`c()`の呼び出しは出来ません。

```javascript:Fallback.sol
// SPDX-License-Identifier: MIT
pragma solidity <0.9.0;

contract Fallback {
    uint256 internal _a;

    constructor(uint256 a_) {
        _a = a_;
    }

    function a() external view returns (uint256) {
        return _a;
    }
}
```

試してみます。

```javascript:scripts/fallback.js
const { ethers } = require("hardhat");

const ABI = [
    "function a() view returns (uint256)",
    "function b() view returns (uint256)",
    "function c() view returns (uint256)",
];

async function main() {
    console.log(">>>>>>>>>>");
    const Fallback = await ethers.getContractFactory("Fallback");
    const fallback = await Fallback.deploy(100);
    const contract = new ethers.Contract(fallback.target, ABI, ethers.provider);
    console.log("a()", await contract.a());
    console.log("b()", await contract.b());
    console.log("c()", await contract.c());
    console.log("<<<<<<<<<<");
}

main().then(() => {
    process.exit(0);
}).catch((error) => {
    console.error(error);
    process.exit(1);
});
```

以下のコマンドで実行してみます。

```powershell
npx hardhat run .\scripts\fallback.js
```

予想通りエラーが出ます。

```
>>>>>>>>>>
a() 100n
Error: Transaction reverted: function selector was not recognized and there's no fallback function
    ＜略＞
```

これに対応する為、Solidityでは`fallback`関数というものが提供されています。


> コントラクトには、`fallback() external [payable]` または `fallback (bytes calldata input) external [payable] returns (bytes memory output)` (どちらも `function` キーワードなし) を使用して宣言された `fallback` 関数を最大 1 つ持つことができます。
この関数は `external` 可視性を持つ必要があります。
フォールバック関数は仮想関数にすることができ、オーバーライドでき、修飾子を持つことができます。
> 
> `fallback` 関数は、他の関数がいずれも指定された関数シグネチャと一致しない場合、またはデータがまったく提供されておらず、[Ether受信関数](https://docs.soliditylang.org/en/v0.8.28/contracts.html#receive-ether-function) がない場合に、コントラクトの呼び出し時に実行されます。
`fallback` 関数は常にデータを受信しますが、Ether も受信するには、`payable` としてマークする必要があります。

https://docs.soliditylang.org/en/v0.8.28/contracts.html#fallback-function

`Fallback.sol`に `fallback` 関数を追加してみます。
単純に `a()` を呼び出すだけの処理とします。

```javascript:Fallback.sol
// SPDX-License-Identifier: MIT
pragma solidity <0.9.0;

contract Fallback {
    uint256 internal _a;

    constructor(uint256 a_) {
        _a = a_;
    }

    function a() external view returns (uint256) {
        return _a;
    }

    fallback(bytes calldata input) external returns (bytes memory output) {
        (bool success, bytes memory data) = address(this).call(
            abi.encodeWithSignature("a()")
        );
        require(success);
        output = data;
    }
}

```

実際に実行してみます。

```powershell
npx hardhat run .\scripts\fallback.js
```

結果を見ると、すべて実行に成功し `a()` と同じ動作をします。

```
>>>>>>>>>>
a() 100n
b() 100n
c() 100n
<<<<<<<<<<
```

このように、コントラクトが実装していない関数を呼び出された時、`fallback` 関数を実装していればこれが呼ばれます。

## Storage

コントラクトの変数はStorageという領域に格納されます。
`StorageA.sol` を見てみます。
`_a`と`_b`という変数がStorageに格納されます。

```javascript:StorageA.sol
// SPDX-License-Identifier: MIT
pragma solidity <0.9.0;

contract StorageA {
    uint256 internal _a;
    uint256 internal _b;

    constructor(uint256 a_, uint256 b_) {
        _a = a_;
        _b = b_;
    }

    function a() external view returns (uint256) {
        return _a;
    }

    function b() external view returns (uint256) {
        return _b;
    }
}
```

どのようにStorageに格納されるのかについてはSolidityのドキュメントに記載されています。

> 動的なサイズの配列とマッピング（下記参照）を除き、データはスロット`0`に格納される最初のステート変数からアイテムごとに連続的に格納される。

https://docs.soliditylang.org/en/v0.8.28/internals/layout_in_storage.html#

値はStorageのスロット（Slot）という番地のような値の場所に格納されます。
`_a`と`_b`は`uint256`なのでスロット `0` と `1` に格納されてるはずです、以下のスクリプトで見てみます。

```javascript:scripts/storage.js
const { ethers } = require("hardhat");

async function main() {
    console.log(">>>>>>>>>>");
    const StorageA = await ethers.getContractFactory("StorageA");
    const storageA = await StorageA.deploy(100, 200);
    console.log("storageA#a()", await storageA.a());
    console.log("storageA#b()", await storageA.b());
    console.log('storageA #0', await ethers.provider.getStorage(storageA.target, 0));
    console.log('storageA #1', await ethers.provider.getStorage(storageA.target, 1));
    console.log("<<<<<<<<<<");
}

main().then(() => {
    process.exit(0);
}).catch((error) => {
    console.error(error);
    process.exit(1);
});
```

以下のコマンドで実行します。

```powershell
npx hardhat run .\scripts\storage.js
```

結果が以下となります。

```text
>>>>>>>>>>
storageA#a() 100n
storageA#b() 200n
storageA #0 0x0000000000000000000000000000000000000000000000000000000000000064
storageA #1 0x00000000000000000000000000000000000000000000000000000000000000c8
<<<<<<<<<<
```

スロット `0` には `0x64` なので `100` となり `a()` と同じ値となります。
スロット `1` には `0xc8` なので `200` となり `b()` と同じ値となります。

`_a`と`_b`という変数の順番を逆（`_b`、`_a`）にしたが `StorageB` を作成します。


```javascript:StorageB.sol
// SPDX-License-Identifier:  MIT
pragma solidity <0.9.0;

contract StorageB {
    uint256 internal _b;
    uint256 internal _a;

    constructor(uint256 a_, uint256 b_) {
        _a = a_;
        _b = b_;
    }

    function a() external view returns (uint256) {
        return _a;
    }

    function b() external view returns (uint256) {
        return _b;
    }
}
```

`StorageA.sol` と　`StorageB.sol` の比較をしてみます。


```javascript:scripts/storage.js
const { ethers } = require("hardhat");

async function main() {
    console.log(">>>>>>>>>>");
    const StorageA = await ethers.getContractFactory("StorageA");
    const storageA = await StorageA.deploy(100, 200);
    console.log("storageA#a()", await storageA.a());
    console.log("storageA#b()", await storageA.b());
    console.log('storageA #0', await ethers.provider.getStorage(storageA.target, 0));
    console.log('storageA #1', await ethers.provider.getStorage(storageA.target, 1));
    const StorageB = await ethers.getContractFactory("StorageB");
    const storageB = await StorageB.deploy(100, 200);
    console.log("storageB#a()", await storageB.a());
    console.log("storageB#b()", await storageB.b());
    console.log('storageB #0', await ethers.provider.getStorage(storageB.target, 0));
    console.log('storageB #1', await ethers.provider.getStorage(storageB.target, 1));
    console.log("<<<<<<<<<<");
}

main().then(() => {
    process.exit(0);
}).catch((error) => {
    console.error(error);
    process.exit(1);
});
```

以下のコマンドで実行します。

```powershell
npx hardhat run .\scripts\storage.js
```

結果が以下となります。

```text
>>>>>>>>>>
storageA#a() 100n
storageA#b() 200n
storageA #0 0x0000000000000000000000000000000000000000000000000000000000000064
storageA #1 0x00000000000000000000000000000000000000000000000000000000000000c8
storageB#a() 100n
storageB#b() 200n
storageB #0 0x00000000000000000000000000000000000000000000000000000000000000c8
storageB #1 0x0000000000000000000000000000000000000000000000000000000000000064
<<<<<<<<<<
```

`a()` と `b()` の結果は同じですが、スロット `0` と `1` は入れ替わっています。
ソースコードに記述する順番によってスロットの場所が変わることを覚えておいてください。

## delegatecall

`fallback` から呼び出される `delegatecall` は以下のような機能となります。

> メッセージ呼び出しには、delegatecall という特別なバリエーションがあります。これは、ターゲット アドレスのコードが呼び出しコントラクトのコンテキスト (つまり、アドレス) で実行され、`msg.sender` と `msg.value` の値が変わらないという点を除けば、メッセージ呼び出しと同じです。
> これは、コントラクトが実行時に別のアドレスからコードを動的にロードできることを意味します。ストレージ、現在のアドレス、残高は引き続き呼び出しコントラクトを参照し、コードのみが呼び出されたアドレスから取得されます。

https://docs.soliditylang.org/en/v0.8.28/introduction-to-smart-contracts.html#delegatecall-and-libraries

`Delegatecall.sol` から `ImplContract.sol` を呼び出してみます。

:::note alert
確認用のコードの為、Proxy コードとしてはNGです。
```javascript:Delegatecall.sol
// SPDX-License-Identifier:  MIT
pragma solidity <0.9.0;

contract Delegatecall {
    address private _impl;

    constructor(address impl_) {
        _impl = impl_;
    }

    fallback(bytes calldata input) external returns (bytes memory output) {
        (bool success, bytes memory data) = address(_impl).delegatecall(input);
        require(success);
        output = data;
    }
}
```

:::

```javascript:ImplContract.sol
// SPDX-License-Identifier:  MIT
pragma solidity <0.9.0;

contract ImplContract {
    address private _a;

    function a() external view returns (address) {
        return _a;
    }

    function sender() external view returns (address) {
        return msg.sender;
    }
}
```

以下のコードで動かしてみます。


```javascript:scripts\delegatecall.js
const { ethers } = require("hardhat");

const ABI = [
    "function a() view returns (address)",
    "function sender() view returns (address)",
];

async function main() {
    console.log(">>>>>>>>>>");
    const [deployer] = await ethers.getSigners();
    const ImplContract = await ethers.getContractFactory("ImplContract");
    const implContract = await ImplContract.deploy();
    const Delegatecall = await ethers.getContractFactory("Delegatecall");
    const delegatecall = await Delegatecall.deploy(implContract.target);
    const contract = new ethers.Contract(delegatecall.target, ABI, deployer);
    console.log('----- address  -----');
    console.log('ImplContract   ', implContract.target);
    console.log('Delegatecall   ', delegatecall.target);
    console.log('deployer       ', deployer.address);
    console.log('----- function -----');
    console.log('sender()       ', await contract.sender());
    console.log('a()            ', await contract.a());
    console.log('----- storage  -----');
    console.log('ImplContract #0', await ethers.provider.getStorage(implContract.target, 0));
    console.log('Delegatecall #0', await ethers.provider.getStorage(delegatecall.target, 0));
    console.log("<<<<<<<<<<");
}

main().then(() => {
    process.exit(0);
}).catch((error) => {
    console.error(error);
    process.exit(1);
});
```

以下のコマンドで実行します。

```powershell
npx hardhat run .\scripts\delegatecall.js
```

結果が以下となります。

```text
>>>>>>>>>>
----- address  -----
ImplContract    0x5FbDB2315678afecb367f032d93F642f64180aa3
Delegatecall    0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
deployer        0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
----- function -----
sender()        0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
a()             0x5FbDB2315678afecb367f032d93F642f64180aa3
----- storage  -----
ImplContract #0 0x0000000000000000000000000000000000000000000000000000000000000000
Delegatecall #0 0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3
<<<<<<<<<<
```

`sender()` と `deployer` が同じ値になります。
通常の `call` だと `Delegatecall` のコントラクトアドレスになりますが、`delegatecall` を使用している為、`Delegatecall` コントラクトを呼び出した `deployer` アドレスが `msg.sender` となります。

ストレージを見てみます。
`ImplContract` コントラクトの変数 `_a` には何も設定していませんが、 `a()` は値を返しています。
`delegatecall` を使用しているので、`Delegatecall` コントラクトのスロット `0` を参照している為です。
Proxy を実装するには Storage に注意する必要があります。

# その他の技術要素

Proxyを利用する場合に気を付けることに、`Storage` と コンストラクタ があります。

## ERC1967

Proxy コントラクトが `Storaga` にデータを格納する時には実装コントラクトが格納するデータとスロットが衝突してはいけません。
Proxy には 実装コントラクトのアドレスを保持する必要があります。
ERC1967は、Proxyコントラクト と 実装コントラクトのストレージ衝突回避用のEIPです。

> プロキシとロジック コントラクト間のストレージ使用の衝突を回避するために、ロジック コントラクトのアドレスは通常、コンパイラによって割り当てられないことが保証されている特定のストレージ スロット (たとえば、OpenZeppelin コントラクトの 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc) に保存されます。この EIP では、プロキシ情報を格納するための一連の標準スロットを提案しています。これにより、ブロック エクスプローラーなどのクライアントは、この情報を適切に抽出してエンド ユーザーに表示でき、ロジック コントラクトは必要に応じてそれに基づいて操作できます。

https://eips.ethereum.org/EIPS/eip-1967

## Initializable

実装コントラクトにはコンストラクターで初期化することができません。
実装コンストラクタのストレージに値を格納してしまうからです。

> プロキシされたコントラクトはコンストラクタを使用しないので、コンストラクタのロジックを外部の `initializer` 関数に移すのが一般的です。
そして、この `initializer` 関数を保護し、一度しか呼び出されないようにする必要があります。
このコントラクトが提供するイニシャライザー修飾子は、この効果をもたらします。

https://docs.openzeppelin.com/contracts/5.x/api/proxy#Initializable

そのため初期化は `delegatecall` 経由で `initializer` を呼び出す必要があります。

# UUPSUpgradeable

実際にProxyコントラクトと実装コントラクトを作成して、upgradeしてみます。

Proxyコントラクトは以下となります、openzeppelinが提供しているERC1967Proxyを使います。

```javascript:TestProxy.sol
// SPDX-License-Identifier: MIT
pragma solidity <0.9.0;

import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract TestProxy is ERC1967Proxy {
    constructor(
        address implementation,
        bytes memory _data
    ) ERC1967Proxy(implementation, _data) {}
}
```

実装コントラクトV1です。
`initialize`関数で初期化を行います。

```javascript:ImplContractV1.sol
// SPDX-License-Identifier: MIT
pragma solidity <0.9.0;

import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

contract ImplContractV1 is UUPSUpgradeable, Initializable {
    address private _owner;
    uint256 internal _a;

    function initialize(uint256 a_) public initializer {
        _owner = msg.sender;
        _a = a_;
    }

    function a() external view virtual returns (uint256) {
        return _a;
    }

    function version() external view virtual returns (uint64) {
        return _getInitializedVersion();
    }

    function _authorizeUpgrade(
        address newImplementation
    ) internal virtual override {
        require(newImplementation != address(0), "ZeroAddress");
        require(_owner == msg.sender, "Unauthorized");
    }
}
```

実装コントラクトV2です。
V1をそのままコピーして`_b`という変数を追加しています。
`initialize2`で初期化します、修飾子 `reinitializer` でバージョン２を指定しています。

```javascript:ImplContractV2.sol
// SPDX-License-Identifier: MIT
pragma solidity <0.9.0;

import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

contract ImplContractV2 is UUPSUpgradeable, Initializable {
    address private _owner;
    uint256 internal _a;

    uint256 internal _b;

    function initialize(uint256 a_) public initializer {
        _owner = msg.sender;
        _a = a_;
    }

    function initialize2(uint256 b_) public reinitializer(2) {
        _b = b_;
    }

    function a() external view virtual returns (uint256) {
        return _a;
    }

    function b() external view virtual returns (uint256) {
        return _b;
    }

    function version() external view virtual returns (uint64) {
        return _getInitializedVersion();
    }

    function _authorizeUpgrade(
        address newImplementation
    ) internal virtual override {
        require(newImplementation != address(0), "ZeroAddress");
        require(_owner == msg.sender, "Unauthorized");
    }
}
```

実装コントラクトV3です。
V2を継承して、`_c`という変数を追加しています。
`initialize3`で初期化します、修飾子 `reinitializer` でバージョン3 を指定しています。


```javascript:ImplContractV3.sol
// SPDX-License-Identifier: MIT
pragma solidity <0.9.0;

import {ImplContractV2} from "./ImplContractV2.sol";

contract ImplContractV3 is ImplContractV2 {
    uint256 internal _c;

    function initialize3(uint256 c_) public reinitializer(3) {
        _c = c_;
    }

    function c() external view virtual returns (uint256) {
        return _c;
    }
}
```

以下のコードで動かしてみます。

```javascript:scripts/proxy.js
const { ethers } = require("hardhat");

const ABI = [
    "function a() view returns (uint256)",
    "function b() view returns (uint256)",
    "function c() view returns (uint256)",
    "function version() view returns (uint64)",
    "function upgradeToAndCall(address newImplementation, bytes memory data)",
    "function initialize(uint256 a_)",
    "function initialize2(uint256 b_)",
    "function initialize3(uint256 c_)",
];

async function main() {
    console.log(">>>>>>>>>>");
    const [deployer] = await ethers.getSigners();
    console.log('deployer', deployer.address);

    const ImplContractV1 = await ethers.getContractFactory("ImplContractV1");
    const implContractV1 = await ImplContractV1.deploy();
    const IMPL_ADDRESS = ethers.toBigInt(ethers.keccak256(ethers.toUtf8Bytes("eip1967.proxy.implementation"))) - 1n;
    const IFACE = new ethers.Interface(ABI);

    const Proxy = await ethers.getContractFactory("TestProxy");
    let data = IFACE.encodeFunctionData("initialize", [100]);
    const proxy = await Proxy.deploy(implContractV1.target, data);
    const contract = new ethers.Contract(proxy.target, ABI, deployer);

    console.log('ImplContractV1', implContractV1.target);
    console.log('version()', await contract.version());
    console.log('Proxy #0', await ethers.provider.getStorage(proxy.target, 0));
    console.log('Proxy #1', ethers.toBigInt(await ethers.provider.getStorage(proxy.target, 1)));
    console.log('Proxy #2', ethers.toBigInt(await ethers.provider.getStorage(proxy.target, 2)));
    console.log('Proxy #3', ethers.toBigInt(await ethers.provider.getStorage(proxy.target, 3)));
    console.log('Proxy ##', await ethers.provider.getStorage(proxy.target, IMPL_ADDRESS));

    // upgrade V2
    const ImplContractV2 = await ethers.getContractFactory("ImplContractV2");
    const implContractV2 = await ImplContractV2.deploy();
    data = IFACE.encodeFunctionData("initialize2", [200]);
    tx = await contract.upgradeToAndCall(implContractV2.target, data);
    receipt = await tx.wait();

    console.log('ImplContractV2', implContractV2.target);
    console.log('version()', await contract.version());
    console.log('Proxy #0', await ethers.provider.getStorage(proxy.target, 0));
    console.log('Proxy #1', ethers.toBigInt(await ethers.provider.getStorage(proxy.target, 1)));
    console.log('Proxy #2', ethers.toBigInt(await ethers.provider.getStorage(proxy.target, 2)));
    console.log('Proxy #3', ethers.toBigInt(await ethers.provider.getStorage(proxy.target, 3)));
    console.log('Proxy ##', await ethers.provider.getStorage(proxy.target, IMPL_ADDRESS));

    // upgrade V3
    const ImplContractV3 = await ethers.getContractFactory("ImplContractV3");
    const implContractV3 = await ImplContractV3.deploy();
    data = IFACE.encodeFunctionData("initialize3", [300]);
    tx = await contract.upgradeToAndCall(implContractV3.target, data);
    receipt = await tx.wait();

    console.log('ImplContractV3', implContractV3.target);
    console.log('version()', await contract.version());
    console.log('Proxy #0', await ethers.provider.getStorage(proxy.target, 0));
    console.log('Proxy #1', ethers.toBigInt(await ethers.provider.getStorage(proxy.target, 1)));
    console.log('Proxy #2', ethers.toBigInt(await ethers.provider.getStorage(proxy.target, 2)));
    console.log('Proxy #3', ethers.toBigInt(await ethers.provider.getStorage(proxy.target, 3)));
    console.log('Proxy ##', await ethers.provider.getStorage(proxy.target, IMPL_ADDRESS));

    console.log('a()', await contract.a());
    console.log('b()', await contract.b());
    console.log('c()', await contract.c());
    console.log("<<<<<<<<<<");
}

main().then(() => {
    process.exit(0);
}).catch((error) => {
    console.error(error);
    process.exit(1);
});
```

以下のコマンドで実行します。

```powershell
npx hardhat run .\scripts\proxy.js
```

結果は以下となります。

```text
>>>>>>>>>>
deployer 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
ImplContractV1 0x5FbDB2315678afecb367f032d93F642f64180aa3
version() 1n
Proxy #0 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
Proxy #1 100n
Proxy #2 0n
Proxy #3 0n
Proxy ## 0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3
ImplContractV2 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
version() 2n
Proxy #0 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
Proxy #1 100n
Proxy #2 200n
Proxy #3 0n
Proxy ## 0x0000000000000000000000009fe46736679d2d9a65f0992f2272de9f3c7fa6e0
ImplContractV3 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
version() 3n
Proxy #0 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
Proxy #1 100n
Proxy #2 200n
Proxy #3 300n
Proxy ## 0x000000000000000000000000dc64a140aa3e981100a9beca4e685f962f0cf6c9
a() 100n
b() 200n
c() 300n
<<<<<<<<<<
```

実装コントラクトのアドレスが変更されていることがわかります。
また、新しく追加した変数も更新されていることがわかります。
結果からアップグレードが出来ていることがわかります。

# まとめ（Conclusion）

Proxyコントラクトの技術要素について、コードも含めてある程度理解できたと思います。
実装するにあたり、実装コントラクト側の実装に注意しないと、`Storage`のスロット衝突が起こってしまうので、注意が必要だと感じました。