決定論的アドレスでアップグレード可能なスマートコントラクトをデプロイする
新しいソーシャルアプリを開発している「Senspace」でCTOをしているりょーまです。今回は決定論的アドレスでスマートコントラクトをデプロイするCREATE2について深掘ります。
なぜ決定論的アドレスが重要なのか
決定論的アドレスとは、デプロイする前に決まっているコントラクトアドレスです。また、EVMチェーンであれば、どのチェーンにデプロイしてもパラメータが同じ限り、同じアドレスでデプロイされます。
複数のスマート・コントラクトが相互に作用する必要があるDeFiプロトコルを構築しているとします。これらのコントラクトを異なるネットワーク(メインネット、テストネット)にデプロイしたとき、ユーザーはそれぞれを間違いなく使用できるようにする必要があります。
ここで決定論的アドレスが重要になります。すべてのネットワークでアドレスが統一されていればヒューマンエラーによる間違いを減らすこともできますし、開発者にとってはデプロイする前にコントラクトのアドレスを知ることができたり、フロントエンドの環境変数を気にしなくていいのでとても便利です。
CREATE2でデプロイする記事はいくつか見かけましたが、今回はアップグレード可能なコントラクト(UUPSで動作検証済み)を決定論的アドレスでデプロイする方法を紹介したいとおもいます。
アップグレード可能なコントラクトについては詳述しません。
クイック・リファレンス
要素 | 目的 | 注意点 |
---|---|---|
Salt | コントラクトアドレスを決定するためのシード値 | デプロイごとに一意でなければならない |
Implementation | 実際のロジックを持っている実態 | 正しく初期化されている必要がある |
Proxy | 関数の実行をImplementationに委任し、データを保持する | 適切なアクセスコントロールが必要 |
CREATE2 | デプロイのためのオペコード | 自身で削除しないと再展開できない |
デプロイまでの大まかなステップ
- ✅ 任意のSaltを生成または定義する
- ✅ Implementationコントラクトアドレスを計算する
- ✅ Implementationコントラクトをデプロイする
- ✅ Proxyコントラクトの初期化データを用意する
- ✅ Proxyコントラクトアドレスを計算する
- ✅ Proxyコントラクトをデプロイする
- ✅ ブロックエクスプローラーでコントラクトを検証する
CREATE2について
CREATE2オペコード(EIP-1014)は、ステートチャネルとオフチェーンのコントラクト作成を容易にするために導入された。 CREATEとは異なり、デプロイする前にコントラクトのアドレスを計算することができるオペコードです。
いくつかの注意点
- CREATE2にはリプレイ攻撃を防ぐ実装はされていません。
- アップグレードコントラクトの初期化関数(Initialize)は保護する必要があります。
- コントラクトのアップグレードには適切なアクセスコントロールが重要です。
- 自己破壊したコントラクトは同じアドレスに再度デプロイ可能です。
- 大規模なバイトコードのデプロイはガスのリミットに引っかかることがあります。
Create2Deployer Contract
CREATE2を使ってデプロイするためのDeployerContractはいくつか存在していますが、私はこちらのDeployerをよく使います。
主な機能
- deploy: CREATE2 を使用してデプロイする。
- computeAddress: デプロイ前にコントラクトアドレスを計算する。
- デプロイ後の追跡のためのイベント発行
// SPDX-License-Identifier: MIT
// Further information: https://eips.ethereum.org/EIPS/eip-1014
pragma solidity ^0.8.9;
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {ERC1820Implementer} from "@openzeppelin/contracts/utils/introspection/ERC1820Implementer.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
/**
* @title CREATE2 Deployer Smart Contract
* @author Pascal Marco Caversaccio, pascal.caversaccio@hotmail.ch
* @dev Helper smart contract to make easier and safer usage of the
* `CREATE2` EVM opcode. `CREATE2` can be used to compute in advance
* the address where a smart contract will be deployed, which allows
* for interesting new mechanisms known as 'counterfactual interactions'.
*/
contract Create2Deployer is Ownable, Pausable {
/**
* @dev Deploys a contract using `CREATE2`. The address where the
* contract will be deployed can be known in advance via {computeAddress}.
*
* The bytecode for a contract can be obtained from Solidity with
* `type(contractName).creationCode`.
*
* Requirements:
* - `bytecode` must not be empty.
* - `salt` must have not been used for `bytecode` already.
* - the factory must have a balance of at least `value`.
* - if `value` is non-zero, `bytecode` must have a `payable` constructor.
*/
function deploy(uint256 value, bytes32 salt, bytes memory code) public whenNotPaused {
Create2.deploy(value, salt, code);
}
/**
* @dev Deployment of the {ERC1820Implementer}.
* Further information: https://eips.ethereum.org/EIPS/eip-1820
*/
function deployERC1820Implementer(uint256 value, bytes32 salt) public whenNotPaused {
Create2.deploy(value, salt, type(ERC1820Implementer).creationCode);
}
/**
* @dev Returns the address where a contract will be stored if deployed via {deploy}.
* Any change in the `bytecodeHash` or `salt` will result in a new destination address.
*/
function computeAddress(bytes32 salt, bytes32 codeHash) public view returns (address) {
return Create2.computeAddress(salt, codeHash);
}
/**
* @dev Returns the address where a contract will be stored if deployed via {deploy} from a
* contract located at `deployer`. If `deployer` is this contract's address, returns the
* same value as {computeAddress}.
*/
function computeAddressWithDeployer(
bytes32 salt,
bytes32 codeHash,
address deployer
) public pure returns (address) {
return Create2.computeAddress(salt, codeHash, deployer);
}
/**
* @dev The contract can receive ether to enable `payable` constructor calls if needed.
*/
receive() external payable {}
/**
* @dev Triggers stopped state.
* Requirements: The contract must not be paused.
*/
function pause() public onlyOwner {
_pause();
}
/**
* @dev Returns to normal state.
* Requirements: The contract must be paused.
*/
function unpause() public onlyOwner {
_unpause();
}
}
デプロイ用のサンプルコード(Hardhat)
Create2Deployerのヘルパー関数
import { ethers, viem } from "hardhat";
import type { Address } from "viem";
// Create2Deployerの最低限のABIを定義
const create2DeployerAbi = [
"function deploy(uint256 amount, bytes32 salt, bytes calldata code) public returns (address)",
"function computeAddress(bytes32 salt, bytes32 codeHash) public view returns (address)",
];
// Create2Deployerの公開されているアドレス
// 独自でDeployerをデプロイしたい場合は書き換える。
const create2DeployerAddress = "0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2";
// すべてのコントラクトで共通で使う任意のSalt、コントラクトごとに変えてもいい
export const baseSalt = ethers.keccak256(ethers.toUtf8Bytes("HogeProtocol"));
// 決定論的アドレスを計算
// テストコードなどで変更できるようにcreate2Deployerはオプショナルで設定できるようにしている。
export const computeAddress = async (
salt: string,
hash: string,
create2Deployer: string = create2DeployerAddress,
) => {
const create2Factory = await ethers.getContractAt(
create2DeployerAbi,
create2Deployer,
);
const computedAddress = await create2Factory.computeAddress(salt, hash);
return computedAddress;
};
// すでに該当コントラクトがデプロイされているかチェック
export const checkAlreadyDeployed = async (address: string) => {
const code = await ethers.provider.getCode(address);
return code !== "0x";
};
// デプロイの実行
// テストコードなどで変更できるようにcreate2Deployerはオプショナルで設定できるようにしている。
export const deployContract_Create2 = async (
salt: string,
code: string,
hash: string,
name: string,
create2Deployer = create2DeployerAddress,
) => {
const computedAddress: Address = await computeAddress(
salt,
hash,
create2Deployer,
);
const alreadyDeployed = await checkAlreadyDeployed(computedAddress);
if (alreadyDeployed) {
console.log(`${name} already deployed at: ${computedAddress}`);
return computedAddress;
}
const create2Factory = await ethers.getContractAt(
create2DeployerAbi,
create2Deployer,
);
const tx = await create2Factory.deploy(0, salt, code);
await tx.wait();
console.log(`${name} deployed at: ${computedAddress}`);
return computedAddress;
};
アップグレーダブルコントラクトデプロイ用のHelper関数
import { ethers, viem } from "hardhat";
import { baseSalt, deployContract_Create2 } from "./Create2Factory";
import ERC1967Proxy from "@openzeppelin/upgrades-core/artifacts/@openzeppelin/contracts-v5/proxy/ERC1967/ERC1967Proxy.sol/ERC1967Proxy.json";
export const deployUpgradeableContract = (
name: string,
initArgs: any[]
) => {
// 1. ImplementationコントラクトをデプロイするためのFactoryを定義し、DeployTransactionのバイトコードなどを取得する。
const ImplFactory = await ethers.getContractFactory(name)
const implTx = await ImplFactory.getDeployTransaction()
// 2. Implementationコントラクトをデプロイする
const implAddress = await deployContract_Create2(
baseSalt,
implTx.data || "0x",
ethers.keccak256(implTx.data),
`${name}_Implementation`
)
// 3. Proxyコントラクトの初期化データを生成する
const initData = ImplFactory.interface.encodeFunctionData("initialize", initArgs)
// 4. ProxyコントラクトをデプロイするためのFactoryを定義し、DeployTransactionのバイトコードなどを取得する。
const proxyFactory = await ethers.getContractFactory(
ERC1967Proxy.abi,
ERC1967Proxy.bytecode,
);
const proxyTx = await UpgradeableProxy.getDeployTransaction(
implAddress,
initData
)
// 5. Proxyコントラクトをデプロイする
const proxyAddress = await deployContract_Create2(
baseSalt,
proxyTx.data || "0x",
ethers.keccak256(proxyTx.data),
name
)
return {
proxyAddress: proxyAddress,
implementationAddress: implAddress,
initData,
}
}
デプロイスクリプト
import { deployUpgradeableContract } from "./deployUpgradeableContract";
const deploy = async = () => {
const result = await deployUpgradeableContract(
"MyToken",
["My Token", "MTK"],
)
}
コントラクトのアップグレードHelper関数
import { baseSalt, deployContract_Create2 } from "./Create2Factory";
import ERC1967Proxy from "@openzeppelin/upgrades-core/artifacts/@openzeppelin/contracts-v5/proxy/ERC1967/ERC1967Proxy.sol/ERC1967Proxy.json";
async function upgradeContract(
proxyAddress: string,
newImplementationName: string
): Promise<string> {
// 1. 新しいImplementationコントラクトをデプロイするためのFactoryを定義し、DeployTransactionのバイトコードなどを取得する。
const Factory = await ethers.getContractFactory(newImplementationName)
const implTx = await Factory.getDeployTransaction()
// 2. 新しいImplementationコントラクトをデプロイする。
const newImplAddress = await deployContract_Create2(
baseSalt,
implTx.data || "0x",
ethers.keccak256(implTx.data),
`${newImplementationName}_Implementation`
)
// 3. Proxyコントラクトに設定してあるImplementationコントラクトアドレスを設定する。
const proxy = await ethers.getContractAt(
ERC1967Proxy.abi,
proxyAddress
)
await proxy.upgradeToAndCall([newImplAddress, "0x"])
return newImplAddress
}
参考ソース
- OpenZeppelin Upgrades Documentation
- Solidity CREATE2 Documentation
- EIP-1014 Specification
- UUPS vs Transparent Proxy Pattern
注意: メインネットにデプロイする前に、必ずテストネットで徹底的にテストし、デプロイスクリプトとコントラクトを本番用に監査してもらうことを検討してください。
もしよければ、X、Farcasterのフォローお願いします!
Discussion