はじめに(Introduction)
JPYC や USDC などのステーブルコインで使用されている、FiatToken
を開発環境で使用したい場合の手順に関する記事です。
本記事では、開発環境を Hardhat
、IDE を VSCode
としています。
FiatToken生成
FiatToken
のソースコードは以下の JPYCv2
を利用します。
開発環境と JPYCv2
では Hardhat
を利用していますが、 JPYCv2
の Hardhat
が最新でない為、コンパイル済みのファイルを開発環境へ移動することとします。
git clone
ワークフォルダ配下にて以下のコマンドを実行してソースコードを取得します。
git clone https://github.com/jcam1/JPYCv2.git
ソースコードを取得したら JPYCv2
フォルダに移動します。
cd JPYCv2
npm install
パッケージをインストールします。
npm install
hardhat.config.js
修正
以下のコマンドで VSCode
を起動します。
code .
hardhat.config.js
を修正します。
修正箇所は、networks
の rinkeby
ブロックをコメントアウトします。
networks: {
hardhat: {
chainId: 1337,
allowUnlimitedContractSize: false,
},
// rinkeby: {
// url: process.env.INFRA_API_KEY,
// accounts: [process.env.PRIVATE_KEY],
// },
},
npx hardhat compile
以下のコマンドを実行してコンパイルを行います。
npx hardhat compile
以下のような結果となります。
※:状況によりコンパイラ(solc
)がダウンロードされる場合があります。
Solidity 0.8.11 is not fully supported yet. You can still use Hardhat, but some features, like stack traces, might not work correctly.
Learn more at https://hardhat.org/reference/solidity-support
Compiling 1 file with 0.4.24
Compiling 30 files with 0.8.11
Compilation finished successfully
ファイル取得
以下の3つのファイルを取得します。
artifacts\contracts\v1\FiatTokenV1.sol\FiatTokenV1.json
artifacts\contracts\v2\FiatTokenV2.sol\FiatTokenV2.json
artifacts\contracts\proxy\ERC1967Proxy.sol\ERC1967Proxy.json
開発環境
開発環境に FiatToken
をデプロイします。
開発環境構築
※:すでに開発環境が存在する場合は割愛してください。
npm
の初期化をします。
npm init -y
hardhat
をインストールします。
npm install hardhat@latest
hardhat
を初期化します。(TypeScript を選択しています。)
npx hardhat init
ファイルの設置
開発環境に FiatToken
フォルダを作成し、コンパイルで作成した3つの json
ファイルを配置します。
work
│ .gitignore
│ hardhat.config.ts
│ package-lock.json
│ package.json
│ README.md
│ tree.txt
│ tsconfig.json
│
├─contracts
├─FiatToken
│ ERC1967Proxy.json
│ FiatTokenV1.json
│ FiatTokenV2.json
├─ignition
├─node_modules
└─test
deployFixture
Hardhat
のテストにおいてFiatToken
のデプロイと初期化をする deployFixture
を作成します。
deployFixture
ヘッダ
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
import { ethers } from "hardhat";
import type { Signer, Contract } from "ethers";
import { expect } from "chai";
import FiatTokenV1Json from "../FiatToken/FiatTokenV1.json";
import FiatTokenV2Json from "../FiatToken/FiatTokenV2.json";
import ERC1967ProxyJson from "../FiatToken/ERC1967Proxy.json";
const TOKEN_NAME = "Dummy Fiat Token";
const TOKEN_SYMBOL = "DFT";
const TOKEN_CURRENCY = "DJPY";
const TOKEN_DECIMALS = 18;
deployFixture
async function deployFixture() {
// アカウントを取得
const [
deployer,
minterAdmin,
minter,
pauser,
blocklister,
rescuer,
owner,
user1,
user2,
user3,
] = await ethers.getSigners();
// FiatTokenV1コントラクトをデプロイ
const FiatTokenV1 = new ethers.ContractFactory(
FiatTokenV1Json.abi,
FiatTokenV1Json.bytecode,
deployer
);
const fiatTokenV1 = await FiatTokenV1.deploy();
await fiatTokenV1.waitForDeployment();
// FiatTokenV2コントラクトをデプロイ
const FiatTokenV2 = new ethers.ContractFactory(
FiatTokenV2Json.abi,
FiatTokenV2Json.bytecode,
deployer
);
const fiatTokenV2 = await FiatTokenV2.deploy();
await fiatTokenV2.waitForDeployment();
// ERC1967Proxyコントラクトをデプロイ、初期化
const ERC1967Proxy = new ethers.ContractFactory(
ERC1967ProxyJson.abi,
ERC1967ProxyJson.bytecode,
deployer
);
const erc1967Proxy = await ERC1967Proxy.deploy(
fiatTokenV1.target, // 初期実装コントラクトのアドレス
fiatTokenV1.interface.encodeFunctionData(
"initialize", // 初期化関数
[
TOKEN_NAME, // トークン名
TOKEN_SYMBOL, // トークンシンボル
TOKEN_CURRENCY, // 通貨名
TOKEN_DECIMALS, // 小数点以下の桁数
minterAdmin.address, // minter管理者アドレス
pauser.address, // pauserアドレス
blocklister.address, // blocklisterアドレス
rescuer.address, // rescuerアドレス
owner.address, // オーナーアドレス
]
)
);
await erc1967Proxy.waitForDeployment();
// Proxy経由でFiatTokenV1コントラクトを操作するためのインスタンスを作成
const fiatTokenV1Proxy = new ethers.Contract(
erc1967Proxy.target,
FiatTokenV1Json.abi,
owner
);
// Proxyコントラクトの実装をFiatTokenV2にアップグレードし、initializeV2を実行
let tx = await fiatTokenV1Proxy.upgradeToAndCall(
fiatTokenV2.target, // 新しい実装コントラクトのアドレス
fiatTokenV2.interface.encodeFunctionData("initializeV2")
);
await tx.wait();
// Proxy経由でFiatTokenV2コントラクトを操作するためのインスタンスを作成
const fiatTokenV2Proxy = new ethers.Contract(
erc1967Proxy.target,
FiatTokenV2Json.abi,
minterAdmin
);
// Minterの初期供給量を設定
tx = await fiatTokenV2Proxy.configureMinter(
minter.address,
ethers.parseUnits("100000000", TOKEN_DECIMALS)
);
await tx.wait();
const token = new ethers.Contract(
erc1967Proxy.target,
FiatTokenV2Json.abi,
minter
);
tx = await token.mint(
user1.address,
ethers.parseUnits("10000", TOKEN_DECIMALS)
);
await tx.wait();
return {
deployer,
minterAdmin,
minter,
pauser,
blocklister,
rescuer,
owner,
user1,
user2,
user3,
fiatTokenV1,
fiatTokenV2,
erc1967Proxy,
};
}
アカウント
各アカウントの説明です。
Account | Description |
---|---|
deployer | コントラクトを deploy するアカウント |
minterAdmin |
mint 実行が可能なアカウントを設定するアカウント |
minter | minterAdmin が設定する mint 実行が可能なアカウント |
pauser |
pause 実行が可能なアカウント |
blocklister | ブロックリストの追加、削除が可能なアカウント |
rescuer |
rescueERC20 実行が可能なアカウント |
owner | 各アカウントを更新可能なアカウント |
user1 | テスト内で使用するユーザー1 |
user2 | テスト内で使用するユーザー2 |
user3 | テスト内で使用するユーザー3 |
デプロイ
ERC1967Proxy
に実装として FiatTokenV1
を設定し、
その後 upgradeToAndCall
にて実装を FiatTokenV2
に変更します。
import
配置した3つの json
ファイルをインポートします。
また、トークンに設定する値を定数として定義します。
import FiatTokenV1Json from "../FiatToken/FiatTokenV1.json";
import FiatTokenV2Json from "../FiatToken/FiatTokenV2.json";
import ERC1967ProxyJson from "../FiatToken/ERC1967Proxy.json";
const TOKEN_NAME = "Dummy Fiat Token";
const TOKEN_SYMBOL = "DFT";
const TOKEN_CURRENCY = "DJPY";
const TOKEN_DECIMALS = 18;
①FiatTokenV1のデプロイ
FiatTokenV1
を実装(implementation)としてデプロイします。
// FiatTokenV1コントラクトをデプロイ
const FiatTokenV1 = new ethers.ContractFactory(
FiatTokenV1Json.abi,
FiatTokenV1Json.bytecode,
deployer
);
const fiatTokenV1 = await FiatTokenV1.deploy();
await fiatTokenV1.waitForDeployment();
②FiatTokenV2のデプロイ
FiatTokenV2
を実装(implementation)としてデプロイします。
// FiatTokenV2コントラクトをデプロイ
const FiatTokenV2 = new ethers.ContractFactory(
FiatTokenV2Json.abi,
FiatTokenV2Json.bytecode,
deployer
);
const fiatTokenV2 = await FiatTokenV2.deploy();
await fiatTokenV2.waitForDeployment();
③ERC1967Proxyデプロイ、初期化
ERC1967Proxy
をデプロイします。
実装(implementation)を FiatTokenV1
とし、initialize
(初期化関数)で初期値を設定しています。
// ERC1967Proxyコントラクトをデプロイ、初期化
const ERC1967Proxy = new ethers.ContractFactory(
ERC1967ProxyJson.abi,
ERC1967ProxyJson.bytecode,
deployer
);
const erc1967Proxy = await ERC1967Proxy.deploy(
fiatTokenV1.target, // 初期実装コントラクトのアドレス
fiatTokenV1.interface.encodeFunctionData(
"initialize", // 初期化関数
[
TOKEN_NAME, // トークン名
TOKEN_SYMBOL, // トークンシンボル
TOKEN_CURRENCY, // 通貨名
TOKEN_DECIMALS, // 小数点以下の桁数
minterAdmin.address, // minter管理者アドレス
pauser.address, // pauserアドレス
blocklister.address, // blocklisterアドレス
rescuer.address, // rescuerアドレス
owner.address, // オーナーアドレス
]
)
);
await erc1967Proxy.waitForDeployment();
④アップグレード
実装(implementation)を FiatTokenV2
にアップグレードします。
FiatTokenV1
の upgradeToAndCall
を利用します。
新しい実装(implementation)を FiatTokenV2
とし、initializeV2
(初期化関数)で新しい実装の初期化をしています。
// Proxy経由でFiatTokenV1コントラクトを操作するためのインスタンスを作成
const fiatTokenV1Proxy = new ethers.Contract(
erc1967Proxy.target,
FiatTokenV1Json.abi,
owner
);
// Proxyコントラクトの実装をFiatTokenV2にアップグレードし、initializeV2を実行
let tx = await fiatTokenV1Proxy.upgradeToAndCall(
fiatTokenV2.target, // 新しい実装コントラクトのアドレス
fiatTokenV2.interface.encodeFunctionData("initializeV2")
);
await tx.wait();
FiatToken
の mint
FiatToken
で mint
するには以下の手順が必要です。
configureMinter
mintAmin
は mint
可能なアカウント(minter
)と初期供給量(100000000
)を設定します。
// Proxy経由でFiatTokenV2コントラクトを操作するためのインスタンスを作成
const fiatTokenV2Proxy = new ethers.Contract(
erc1967Proxy.target,
FiatTokenV2Json.abi,
minterAdmin
);
// Minterの初期供給量を設定
tx = await fiatTokenV2Proxy.configureMinter(
minter.address,
ethers.parseUnits("100000000", TOKEN_DECIMALS)
);
await tx.wait();
mint
minter
が ユーザー(user1
)にトークン(10000
)を mint
します。
// Proxy経由でFiatTokenV2コントラクトを操作するためのインスタンスを作成
const token = new ethers.Contract(
erc1967Proxy.target,
FiatTokenV2Json.abi,
minter
);
// ミント
tx = await token.mint(
user1.address,
ethers.parseUnits("10000", TOKEN_DECIMALS)
);
await tx.wait();
FiatTokenの転送
ユーザーがトークンを転送する方法をいくつか試してみます。
transfer
ユーザー(user1
)が自身のトークンを他のユーザー(user2
)に transfer
(転送)します。
トランザクション送信者はユーザー(user1
)となります。
コード)
it("transfer", async function () {
// デプロイされたコントラクトとユーザーアカウントを取得
const { erc1967Proxy, user1, user2 } = await loadFixture(deployFixture);
// FiatTokenV2コントラクトを操作するためのインスタンスを作成
let token = new ethers.Contract(
erc1967Proxy.target,
FiatTokenV2Json.abi,
user1
);
// ユーザー1とユーザー2の残高を確認
expect(await token.balanceOf(user1.address)).to.equal(
ethers.parseUnits("10000", TOKEN_DECIMALS)
);
expect(await token.balanceOf(user2.address)).to.equal(
ethers.parseUnits("0", TOKEN_DECIMALS)
);
// ユーザー1からユーザー2にトークンを転送
let tx = await token.transfer(
user2.address,
ethers.parseUnits("1000", TOKEN_DECIMALS)
);
let receipt = await tx.wait();
expect(receipt.status).to.equal(1);
// ユーザー1とユーザー2の残高を確認
expect(await token.balanceOf(user1.address)).to.equal(
ethers.parseUnits("9000", TOKEN_DECIMALS)
);
expect(await token.balanceOf(user2.address)).to.equal(
ethers.parseUnits("1000", TOKEN_DECIMALS)
);
});
transferWithAuthorization
ユーザー(user1
)が自身のトークンを他のユーザー(user2
)に transferWithAuthorization
(転送)します。
ユーザー(user1
)は、送信を許可する署名値をトランザクション送信者(user3
)に渡します。
トランザクション送信者(user3
)は署名値を使用して transferWithAuthorization
(転送)を実行します。
コード)
it("transferWithAuthorization", async function () {
// デプロイされたコントラクトとユーザーアカウントを取得
const { erc1967Proxy, user1, user2, user3 } = await loadFixture(
deployFixture
);
// FiatTokenV2コントラクトを操作するためのインスタンスを作成
let token = new ethers.Contract(
erc1967Proxy.target,
FiatTokenV2Json.abi,
user3
);
// ユーザー1とユーザー2の残高を確認
expect(await token.balanceOf(user1.address)).to.equal(
ethers.parseUnits("10000", TOKEN_DECIMALS)
);
expect(await token.balanceOf(user2.address)).to.equal(
ethers.parseUnits("0", TOKEN_DECIMALS)
);
// EIP712のドメインを設定
const domain = {
name: await token.name(), // ドメイン名
version: "2", // ドメインバージョン
verifyingContract: token.target as string, // コントラクトアドレス
chainId: (await ethers.provider.getNetwork()).chainId, // チェーンID
};
// TransferWithAuthorizationのタイプを定義
const TYPES = {
TransferWithAuthorization: [
{ name: "from", type: "address" }, // 支払アドレス (承認者)
{ name: "to", type: "address" }, // 受取アドレス
{ name: "value", type: "uint256" }, // 送金金額
{ name: "validAfter", type: "uint256" }, // この時間の後で有効 (UNIX 時間)
{ name: "validBefore", type: "uint256" }, // この時間の前で有効 (UNIX 時間)
{ name: "nonce", type: "bytes32" }, // 一意の nonce (32 バイト)
],
};
// TransferWithAuthorizationの値を定義
const value = {
from: user1.address, // 支払アドレス (承認者)
to: user2.address, // 受取アドレス
value: ethers.parseUnits("1000", TOKEN_DECIMALS), // 送金金額
validAfter: 0n, // この時間の後で有効 (UNIX 時間)
validBefore: BigInt(Math.floor(Date.now() / 1000)) + 3600n, // この時間の前で有効 (UNIX 時間)
nonce: ethers.randomBytes(32), // 一意の nonce (32 バイト)
};
// EIP712の署名を取得
const sign = ethers.Signature.from(
await user1.signTypedData(domain, TYPES, value)
);
// ユーザー1からユーザー2にトークンを転送
let tx = await token.transferWithAuthorization(
value.from, // 支払アドレス (承認者)
value.to, // 受取アドレス
value.value, // 送金金額
value.validAfter, // この時間の後で有効 (UNIX 時間)
value.validBefore, // この時間の前で有効 (UNIX 時間)
value.nonce, // 一意の nonce (32 バイト)
sign.v, // 署名の v 値
sign.r, // 署名の r 値
sign.s // 署名の s 値
);
let receipt = await tx.wait();
expect(receipt.status).to.equal(1);
// ユーザー1とユーザー2の残高を確認
expect(await token.balanceOf(user1.address)).to.equal(
ethers.parseUnits("9000", TOKEN_DECIMALS)
);
expect(await token.balanceOf(user2.address)).to.equal(
ethers.parseUnits("1000", TOKEN_DECIMALS)
);
});
receiveWithAuthorization
ユーザー(user1
)が自身のトークンを他のユーザー(user2
)に receiveWithAuthorization
(転送)します。
ユーザー(user1
)は、送信を許可する署名値をトランザクション送信者(user2
)に渡します。
トランザクション送信者(user2
)は署名値を使用して receiveWithAuthorization
(転送)を実行します。
※:transferWithAuthorization
との違いは、トランザクション送信者(user2
)がトークンの受取ユーザー(user2
)と同じ必要があることです。
コード)
it("receiveWithAuthorization", async function () {
// デプロイされたコントラクトとユーザーアカウントを取得
const { erc1967Proxy, user1, user2, user3 } = await loadFixture(
deployFixture
);
// FiatTokenV2コントラクトを操作するためのインスタンスを作成
let token = new ethers.Contract(
erc1967Proxy.target,
FiatTokenV2Json.abi,
user2
);
// ユーザー1とユーザー2の残高を確認
expect(await token.balanceOf(user1.address)).to.equal(
ethers.parseUnits("10000", TOKEN_DECIMALS)
);
expect(await token.balanceOf(user2.address)).to.equal(
ethers.parseUnits("0", TOKEN_DECIMALS)
);
// EIP712のドメインを設定
const domain = {
name: await token.name(), // ドメイン名
version: "2", // ドメインバージョン
verifyingContract: token.target as string, // コントラクトアドレス
chainId: (await ethers.provider.getNetwork()).chainId, // チェーンID
};
// ReceiveWithAuthorizationのタイプを定義
const TYPES = {
ReceiveWithAuthorization: [
{ name: "from", type: "address" }, // 支払アドレス (承認者)
{ name: "to", type: "address" }, // 受取アドレス
{ name: "value", type: "uint256" }, // 送金金額
{ name: "validAfter", type: "uint256" }, // この時間の後で有効 (UNIX 時間)
{ name: "validBefore", type: "uint256" }, // この時間の前で有効 (UNIX 時間)
{ name: "nonce", type: "bytes32" }, // 一意の nonce (32 バイト)
],
};
// ReceiveWithAuthorizationの値を定義
const value = {
from: user1.address, // 支払アドレス (承認者)
to: user2.address, // 受取アドレス
value: ethers.parseUnits("1000", TOKEN_DECIMALS), // 送金金額
validAfter: 0n, // この時間の後で有効 (UNIX 時間)
validBefore: BigInt(Math.floor(Date.now() / 1000)) + 3600n, // この時間の前で有効 (UNIX 時間)
nonce: ethers.randomBytes(32), // 一意の nonce (32 バイト)
};
// EIP712の署名を取得
const sign = ethers.Signature.from(
await user1.signTypedData(domain, TYPES, value)
);
// ユーザー1からユーザー2にトークンを転送
let tx = await token.receiveWithAuthorization(
value.from, // 支払アドレス (承認者)
value.to, // 受取アドレス
value.value, // 送金金額
value.validAfter, // この時間の後で有効 (UNIX 時間)
value.validBefore, // この時間の前で有効 (UNIX 時間)
value.nonce, // 一意の nonce (32 バイト)
sign.v, // 署名の v 値
sign.r, // 署名の r 値
sign.s // 署名の s 値
);
let receipt = await tx.wait();
expect(receipt.status).to.equal(1);
// ユーザー1とユーザー2の残高を確認
expect(await token.balanceOf(user1.address)).to.equal(
ethers.parseUnits("9000", TOKEN_DECIMALS)
);
expect(await token.balanceOf(user2.address)).to.equal(
ethers.parseUnits("1000", TOKEN_DECIMALS)
);
});
まとめ
FiatToken
を利用したコントラクトやアプリを作成するための準備ができると思います。