こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
今回は「ERC20Permit」について解説していきます!
「ERC20Permit」を理解する上で必要な知識についてもこの記事では取り上げているため、過去に理解できなかった人でもこの記事であれば理解できるようにしています。
丁寧に解説している分だいぶ長くなってしまっているので時間があるときに一気に読むのをお勧めします。
それでは早速中身を見ていきましょう!
ERC20
ERC20Permitを理解するには、まず前提としてERC20について理解しておく必要があります。
ERC20については以下の記事にまとめているので、まだERC20についてしっかり理解できていない人はぜひ読んでみてください。
トランザクションとは?
次にトランザクションが何かを理解する必要があります。
この部分はなんとなく理解している人が多いのではないでしょうか?
トランザクションについて、bitbankの用語集には以下のように書かれています。
仮想通貨におけるトランザクション(Transaction)とは簡単にいえば取引のことで、前の持ち主から受け取った取引のハッシュ値と、送り先のアドレスなどを含め、所有者の秘密鍵で電子署名したものを言います。
https://bitbank.cc/glossary/transaction
トランザクションとは、一言で言えば「ある特定の取引」のことであることがわかります。
トランザクションには、送金したいユーザーの所有する秘密鍵を使用して作成した署名と、送作のアドレスや1つ前のトランザクションのハッシュ値、取引内容などのデータが入っています。
上記はビットコインの例で、Ethereumの場合はトランザクションに含まれるデータが異なってきます。
Ethereumの場合は、ガス価格や送信先のアドレス、送信するETHの量、トランザクションを発行するアドレスによる署名などがトランザクションに含まれています。
そもそもビットコインとEthereumでトランザクションの構成が違うことを初めて知った人もいるのではないでしょうか?
UTXO
ビットコインはUTXO(Unspent Transaction Output)という仕組みを使用して、コインの残高や取引履歴を管理しています。
UTXOとは、「未使用トランザクションアウトプット」と呼ばれ、「特定の所有者が保持している分割不可能なビットコインの塊」としてブロックチェーンに記録されています。
つまり、ビットコインはどこか口座などに残高として記録されているわけではなく、取引データの中に散らばっているUTXOを集めて残高を計算しています。
もう少しだけ詳しく説明すると、取引データはインプットとアウトプットの2つから構成され、トランザクションのインプットの合計額とアウトプットの合計額は等しくなります。
このアウトプットのトランザクションが「未使用トランザクションアウトプット(UTXO)」であり、次に受け取る人にとってはインプットとなります。
ブロックの中の取引データはこれが永遠に繰り返されています。
具体例を見ていきましょう。
Aさんが合計30BTC
を持っているとします。
ただ、先ほど説明したようにインプット部分では30BTC
ではなく、複数のインプットに分割されています。
AさんからBさんに10BTC
を支払うとします。
この時10BTC
だけ渡すのではなく、分割された25BTC
全てを渡し、計算して余った15BTC
を送り元のAさんに再度送っています。
ビットコインではこのように、インプットの時点で一旦全ての資金を消費し、再度自分自身や送り先の別のアドレスに資金を送っています。
上記の説明を図にしたのが以下の図になります。
左がインプットで右がアウトプットです。
インプットとアウトプットの合計金額が同じなのが確認できます。
ビットコインのブロックではこれが繰り返されていっています。
この記事ではこれ以上深く取り扱わないので、より詳しくは以下の記事を参考にしてください。
アカウントステート
EthereumではUTXOというモデルを使用せずに、アカウントステートというモデルを使用しています。
アカウントステートを理解する前に2つのアカウントについて解説しています。
EthereumにはEOA(外部所有)アカウントとコントラクトアカウントの2つが存在します。
EOAアカウント
ウォレットに紐づく、秘密鍵によって制御されています。
トランザクションを生成することができます。
コントラクトアカウント
秘密鍵を持っておらず、スマートコントラクトの実行コードを持っていて、スマートコントラクトによって制御されています。
ETHを受け取ることはできますが、トランザクションを生成することができません。
では、アカウントステートに話を戻しましょう。
Ethereumはトランザクションに基づいたステート(状態)という概念があり、新たにトランザクションが生成されるごとにステート(状態)が変化していきます。
このステート(状態)のことをアカウントステートといいます。
アカウントステートは以下の4つのデータを持っています。
データ
- nonce: アカウントが送ったトランザクションの数。コントラクトアカウントの場合は、アカウントが作成したコントラクトの数。
- Balance: アドレスが持っている資金の量。
- Storage Root: アカウントが持っているデータのマークルツリーのルートハッシュ。
- Code Hash: EOAの場合は空文字のハッシュで、コントラクトアカウントの場合は、そのアカウントが所有しているコードのハッシュ。
マークルツリーについては以下の記事を参考にしてください。
ビットコインでは、残高のデータではなく、残高を出すのに必要な計算データだけ持っています。それに対しEthereumでは、残高を出すのに必要なデータに加え、残高も保持しているためUTXOと比べてデータ量が多くなります。
これはEthereumではスマートコントラクトを使用してアプリケーションを作成できるため、各トランザクションからその時点の残高を取得しやすいようにしているのではないかと考えています。
ちなみにUTXOでは、アウトプットから次のインプットが作られるため、複数の全く同じトランザクションが通る心配はないが、Ethereumだと通ってしまいます。
そのため、nonce
という「何番目のトランザクション」かというデータを含めることで、複数の全く同じトランザクションが通らくなるようにしています。
例えばnonce
の値が100だとして、同じ100というnonce
を持つデータは処理されず、同じ処理でもnonce
が101であれば別のトランザクションとみなされて処理されます。
ビットコインは先ほど説明したように、インプットの値を全て送り、送り先がないものについては自分自身に送っているため、同じ処理を複数回通すことがデータの構造上不可能です。
一方、Ethereumの場合は、ステート(状態)でデータを管理しているため、十分な資金があれば同じ処理を通すことができてしまうので、nonce
という値を用いてそれを防いでいます。
前提知識としてトランザクションについて解説しましたが、ここだけでだいぶ長くなってしまいました…。
疲れた人は一旦休憩して先を読んでください!
メタトランザクションとは
次にメタトランザクションについて理解していきましょう。
メタトランザクションとは、簡単にいうと「ガスを支払うことなくトランザクションを実行できる」ことです。
ガスを支払う必要がないとなると誰にとってもメリットに感じますよね。
ただし実際ガス代が全く不要になるわけではありません。
正確には、以下の画像のように支払うガス代を第3者に肩代わりしてもらっています。
そもそもガス代を仲介してくれるユーザーなんているのでしょうか?
例えば、ブロックチェーンゲームにおいてユーザーにガス代を負担させてしまうと、UXが悪くなります。
そこで運営がガス代を負担することで、ユーザーは気軽にゲームを楽しむことができるようになります。
ではどのようにしてガス代を第3者に負担してもらっているのでしょうか?
ブロックチェーンゲームを想定すると、手順は以下になります。
手順
- ユーザー向けのフロントエンドを作成しユーザーに提供。
- ユーザーは自分が所有している秘密鍵でメッセージに署名する。ただし、署名のみのためガスがかからず、署名済みのメッセージと署名の情報を運営に送る。
- 運営はメッセージの署名情報と
nonce
値を確認し、問題なければ運営が所有している秘密鍵で署名しトランザクションを生成して、コントラクトを呼び出して実行する。 - 運営は正常にトランザクションが実行されたかを確認できたら、ユーザーにも実行が完了したことを通知する。
上記を図にすると以下のようになります。
このようにするとユーザーは秘密鍵を知られず、ガス代もかからず処理を実行できます。
この一連の処理は、EVMが行なっていることをスマートコントラクトで実行しているだけとも言えます。
ECDSAという方式でユーザーのアドレスが持つ秘密鍵で署名をし、署名を受け取ったコントラクトで署名をもとに公開鍵を導き出して、どのユーザーが署名をしたのか検証してトランザクションを実行しています。
以上がメタトランザクションについての説明です。
ERC20Permitとは
ではやっと本題のERC20Permitの説明に入っていきます。
ERC20Permitとは、ERC20の拡張機能です。
通常、transferFrom
(特定のアドレスから特定のアドレスへERC20トークンを送る)関数を呼び出してERC20トークンを送るとき、あらかじめapprove
(自分が所有しているERC20トークンを送る許可を他のアドレスに与える)関数をトランザクションで呼び出す必要があります。
この手順はユーザーからすると手間がかかります。
なぜなら2つのトランザクションが発生するため、2回ウォレットのサインを求められ、ガス代を支払うために十分なETHを保有する必要があります。
この問題を解決するのがERC20Permitです。
ERC20Permitのざっくりとした処理は以下の手順で行われます。
手順
- オフチェーン上でユーザーのウォレットの秘密鍵でメッセージに署名してもらい、署名データと署名したメッセージの情報をコントラクトに渡しながら、ガス代を負担するアドレスで署名してトランザクションを生成する。
- コントラクトでは、渡された署名データと署名したメッセージの情報をもとに、署名したアドレスの公開鍵を再生成し、送りたいERC20トークンの保有者か確認します。
- 確認ができたのち、トランザクションを生成し、
approve
関数を実行して他のアドレスに署名したアドレスが保有するERC20トークンの指定量分の送金許可を別アドレスに許可します。 - ERC20トークンの送金許可されたアドレスから、
transferFrom
関数を実行して別のアドレスへ実際にERC20トークンを送金します。
このメリットとしては、ユーザーはETHを保有する必要がなく、ガス代も負担する必要がない点です。
また、approve
関数の処理とtransferFrom
関数の処理を1つのトランザクションにまとめることができ、トランザクションの回数が1回で済みます。
署名部分ではEIP712の理解が必要なので今後別記事でまとめていきます。
ちなみになぜtransfer
関数を実行して直接ERC20トークンを送ることをせず、approve
関数とtransferFrom
関数を実行するという一見回りくどい方法をとっているのでしょう?
この理由としては、ERC20Permit自体がDEXとコントラクトのより使いやすくするために提案されたものなのであり、もしtransfer
関数を実装すると、開発者はDEXの処理を変更しなければいけなくなってしまうためです。
EIP-2612: Why doesn't it use _transfer instead of approvals
EIP2612
EIPとは
EIPとは 「Ethereum Improvement Proposals」の略で、「Ethereumの新しい機能やプロセスに関する提案を規定する標準規格」のことです。
EIPの細かい説明は以下に書かれています。
簡単に言うと以下になります。
EIPは、Ethereum Improvement Proposalsの略でイーサリアムをより良いものにするために議論される改善提案のこと。イーサリアムは、特定の誰かによって管理されるものではないため、世界中の誰もがEIPを提出することで、イーサリアムの発展に貢献することができる。
20
という数字は20
番目の提案ということです。
EIP2162
EIP2162とは、ERC20Permitの元となった提案です。
ERC20に以下の3つの関数を追加しています。
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external
function nonces(address owner) external view returns (uint)
function DOMAIN_SEPARATOR() external view returns (bytes32)
1つずつ紹介していきます。
permit
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external
ERC20トークンの保有者かつ、オフチェーンで署名したユーザーのアドレスがowner
に渡されます。
spender
には、ERC20トークンの送金許可を与えたりアドレスを格納します。
value
にはどれくらいの量のERC20トークンの送金許可を与えるか指定し、deadline
にはいつまでに処理が有効か渡します。
v
, r
, s
には、署名したユーザーのアドレスの公開鍵を復元するために必要な情報が格納されます。
この関数で前章で解説した処理を実行しています。
条件
- 現在のタイムスタンプは
deadline
以下である。 owner
には0アドレスを指定できない。- アドレスごとの
nonce
値を格納しているnonces
配列の値が、配列の更新前と等しい。 v
,r
,s
は、メッセージのowner
による有効なsecp256k1署名である。
secp256k1署名について詳しくは以下の記事が参考になります。
nonces
function nonces(address owner) external view returns (uint)
アドレスごとのnonce
値を格納しているnonces
配列から、owner
に渡されたアドレスのnonce
値を返す関数。
nonce
というトランザクションごとに異なる値を使用することで、同じトランザクションを複数回と生成されるのを防ぐことができます。
DOMAIN_SEPARATOR
function DOMAIN_SEPARATOR() external view returns (bytes32)
EIP712で管理されているメタデータのハッシュ値を返す関数。
メタデータは以下のような構造体になっています。
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Permit": [{
"name": "owner",
"type": "address"
},
{
"name": "spender",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
}
],
"primaryType": "Permit",
"domain": {
"name": erc20name,
"version": version,
"chainId": chainid,
"verifyingContract": tokenAddress
},
"message": {
"owner": owner,
"spender": spender,
"value": value,
"nonce": nonce,
"deadline": deadline
}
}}
この情報は署名の検証に必要となります。
ERC20Permitのコード確認
EIP20Permitについて確認してきたので、この章では実際にERC20Permitのコードを確認していきましょう。
今回はOpenzeppelinの以下のコードをもとに1つずつ解説していきます。
読み込み
pragma solidity ^0.8.0;
import "./IERC20Permit.sol";
import "../ERC20.sol";
import "../../../utils/cryptography/ECDSA.sol";
import "../../../utils/cryptography/EIP712.sol";
import "../../../utils/Counters.sol";
Solidityのバージョンと他のコントラクトを読み込んでいます。
IERC20Permit.sol
ERC20PermitのInterface
が定義されています。
interface
とは、コントラクトに似ていますが実行することはできません。
interface
には関数名やひきすうなどのみ定義されていて中身は一切定義されていません。
そのため、interface
を継承したコントラクト内で関数を再度定義して中身を記述する必要があります。
「interface
いらなくね?」
こう思う方もいると思います。
interface
を使用するメリットは、実装で必要な関数を確認できることにあります。
最初で述べたように、interface
には実装する上で必要な関数が定義されているので、開発者やコントラクトを実行するユーザーからどんな機能があるのかを簡単に確認できます。
有名なプロジェクトでもinterface
はよく使用されているので、この機会に理解しておくと後々役に立ちます。
公式ドキュメントは以下になります。
https://solidity-jp.readthedocs.io/ja/latest/contracts.html#interfaces
ERC20.sol
ERC20コントラクトを定義しています。
ERC20トークンを発行したり、送ったりできます。
ECDSA.sol
メッセージに署名したアドレスを復元するrecover
関数を使用するために読み込んでいます。
EIP712.sol
ユーザーは、EIP712のデータ構造を使用してメッセージに署名をして渡します。
Counters.sol
安全にインクリメント・デクリメントができるカウンタを使用するために読み込んでいます。
変数定義
abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712 {
using Counters for Counters.Counter;
mapping(address => Counters.Counter) private _nonces;
bytes32 private constant _PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 private _PERMIT_TYPEHASH_DEPRECATED_SLOT;
ERC20Permitという名前のコントラクトを定義していますが、abstract
というものがついています。
これはabstract
コントラクトを他のコントラクトで継承してもらい、abstract
コントラクト内では中身がない関数を定義し、継承先で必ず上書きしないといけないにすることができます。
ERC20Permit内には中身が記述されていない関数がないため、他の理由でabstract
にしているはずですが明確な理由はわかりませんでした。
_nonces
各アドレスごとのnonce
値を格納。
_PERMIT_TYPEHASH
署名したアドレスを生成するために必要なデータのハッシュ値です。
_PERMIT_TYPEHASH_DEPRECATED_SLOT
_PERMIT_TYPEHASH
変数はconstant
とついているように変更ができません。
そのため、_PERMIT_TYPEHASH
を変更できるように定義した変数。
constructor
constructor(string memory name) EIP712(name, "1") {}
EIP712コントラクトのconstructor
を実行。
name
はトークン名で、version
に1
を指定しています。
permit
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual override {
require(block.timestamp <= deadline, "ERC20Permit: expired deadline");
bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, v, r, s);
require(signer == owner, "ERC20Permit: invalid signature");
_approve(owner, spender, value);
}
owner
に渡されたアドレスと署名したアドレスが同じかを検証し、spender
に渡されたアドレスにowner
が所有するERC20トークンをvalue
分transfer
関数を実行して送金する許可を与える関数。
10行目では、現在のタイムスタンプがdeadline
に渡された値より小さいか確認しています。
12行目では、署名したアドレスを再生成するために、EIP712に順序した構造体データをエンコードしてハッシュ化しています。
14行目で、ハッシュ化された構造体データを完全にエンコードしたハッシュを返します。
16行目で、先ほどのハッシュ化したデータとv
、r
、s
というトランザクションの署名と公開鍵の復元に関係する値(正確にはr
とs
はECDSA署名の出力で、v
はリカバリーIDと呼ばれる値)を渡して、署名したアドレスの公開鍵を取得しています。
17行目で、署名したアドレスと復元したアドレスが同じか確認し、同じであれば19行目で_approve
関数を実行して、spender
に渡されたアドレスにowner
が所有するERC20トークンをvalue
分transfer
関数を実行して送金する許可を与えています。
nonces
function nonces(address owner) public view virtual override returns (uint256) {
return _nonces[owner].current();
}
_nonces
配列からowner
に渡されたアドレスのnonce
値を取得する関数。
DOMAIN_SEPARATOR
function DOMAIN_SEPARATOR() external view override returns (bytes32) {
return _domainSeparatorV4();
}
EIP712で管理されているメタデータのハッシュ値を返す関数。
_useNonce
function _useNonce(address owner) internal virtual returns (uint256 current) {
Counters.Counter storage nonce = _nonces[owner];
current = nonce.current();
nonce.increment();
}
_nonces
配列のowner
に渡されたアドレスの現在のnonce
値を返しインクリメントする関数。
ERC20Permitの実装
では次にERC20Permitを実装していきましょう!
以下のOpenZeppelinのWizardを使用すると簡単にERC20Permitを実装することができます。
https://wizard.openzeppelin.com/
ERC20のタブを選択し、「FEATURES」メニュのPermitにチェックを入れるだけです。
今回1から実装するのが大変だったので、以下の方のコードを参考にさせていただきました🙏🏻
https://github.com/t4sk/hello-erc20-permit
環境構築
まずは最低限の部分を実装していきましょう!
以下の記事の「環境構築」の章を参考にして進めてください。
上記完了しましたら、最後にhardhat.config.js
の中身を以下にしてください。
require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: {
version: "0.8.17",
settings: {
optimizer: {
enabled: true,
runs: 1000,
},
},
},
};
コントラクト作成
雛形は作成できたので、次にコントラクトを作成していきます。
CardeneToken.sol
まずはERC20トークンを発行するコントラクトを作成していきましょう。
ERC20Permitは、ERC20と含まれているようにERC20トークンを扱います。
そのため、ERC20トークンをあらかじめ用意しておく必要があるので、簡単にサクッとコードを書いていきます。
以下のコマンドを実行してファイルを作成してください。
$ mkdir contracts/CardeneToken.sol
CardeneToken.sol
ファイルを作成したら、以下を貼り付けてください。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract CardeneToken is ERC20 {
// トークン名は"Cardene Token"、トークンシンボルは"CARD"。
constructor() ERC20("Cardene Token", "CARD") {}
/// @notice CARDトークンを発行する関数。
/// @ param _to 発行したトークンを送るアドレス。
/// @ param _amount 発行するトークン数。
function mint(address _to, uint _amount) external {
_mint(_to, _amount);
}
}
これでERC20トークンを発行するコードは完成です。
このファイルは今回使いません。参考のために作成しています。
思ったより短く感じたのではないでしょうか?
IERC20Permit.sol
次にERC20Permitのinterface
を作成していきます。
以下のコマンドを実行してファイルを作成してください。
$ mkdir contracts/IERC20Permit.sol
ファイルを作成したら、以下をファイル内に貼り付けてください。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
interface IERC20Permit {
/// @notice ERC20トークンの総量を返す関数。
function totalSupply() external view returns (uint);
/// @notice accountに渡されたアドレスが所有しているERC20トークンの量を返す関数。
/// @param account 特定のアドレス。
/// @return アドレスが所有しているERC20トークンの量。
function balanceOf(address account) external view returns (uint);
/// @notice recipientに渡されたアドレスに、amount分のERC20トークンを送る関数。
/// @param recipient ERC20トークンの送り先アドレス。
/// @param amount 送りたいERC20トークンの量。
/// @return 送金が成功したか失敗したかのbool値。
function transfer(address recipient, uint amount) external returns (bool);
/// @notice spenderに渡されたアドレスが、ownerが所有するERC20トークンをどれくらい送る許可を与えられているか返す関数。
/// @param owner ERC20トークンの所有者アドレス。
/// @param spender ownerが所有するERC20トークンをどれくらいの量送る許可を与えられているか確認したいアドレス。
/// @return ERC20トークンを送れる許可を与えられている量。
function allowance(address owner, address spender) external view returns (uint);
/// @notice spenderで指定されたアドレスに、amout分のERC20トークンの送る許可を与える関数。
/// @param spender ERC20トークンを送る許可を与えたいアドレス。
/// @param amount 許可を与えるERC20トークンの量。
/// @return 実行が成功したかのbool値。
function approve(address spender, uint amount) external returns (bool);
/// @notice spenderに渡されたアドレスから、recipientに渡されたアドレスへamount分のERC20トークンを送る関数。
/// @param sender ERC20トークンの所有者アドレス。
/// @param recipient senderのERC20トークンを送る許可を与えられているアドレス。
/// @param amount 送りたいERC20トークンの量。
/// @return 実行が成功したかのbool値。
function transferFrom(
address sender,
address recipient,
uint amount
) external returns (bool);
/// @notice 第3者アドレスがERC20トークンを送る関数。
/// @param owner ERC20トークンの所有者アドレス。
/// @param spender ERC20トークンを代わりに送るアドレス。(ガス代を代わりに負担)
/// @param value 送りたいERC20トークンの量。
/// @param deadline 送れる期限。
/// @param v リカバリーID。
/// @param r ECDSA署名の出力。
/// @param s ECDSA署名の出力。
function permit(
address owner,
address spender,
uint value,
uint deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
// イベント
event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
}
各関数の説明や引数の説明をコメントしているので、気になる方はそちらを確認してください。
このファイルは今回使いません。参考のために作成しています。
Bank.sol
次にERC20Permitを作成していきます。
以下のコマンドを実行してファイルを作成してください。
$ mkdir contracts/Bank.sol
Bank.sol
ファイルを作成したら、以下を貼り付けてください。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
contract Bank is ERC20, Ownable, ERC20Permit {
// ERC20とERC20Permitのconstructorを呼び出しています。
constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {}
/// @notice ERC20トークンを発行する関数。
/// @param to ERC20トークンを送りたいアドレス。
/// @param amount 発行するERC20トークンの量。
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
/// @notice msg.senderからコントラクトにamount分のERC20トークンを送る関数。
/// @param amount 送りたいERC20トークンの量。
function deposit(uint amount) external {
transferFrom(msg.sender, address(this), amount);
}
/// @notice permit関数を実行し、第3者がガス代を負担して、特定のアドレスにERC20トークンを送る関数。
/// @param amount 送りたいERC20トークンの量。
/// @param deadline 実行できる期限。
/// @param v リカバリーID。
/// @param r ECDSA署名の出力。
/// @param s ECDSA署名の出力。
function depositWithPermit(address _owner, address _spender, uint amount, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
permit(_owner, _spender, amount, deadline, v, r, s);
require(allowance(_owner, _spender) > 0, "There are no tokens allowed for transfer.");
transferFrom(_owner, _spender, amount);
}
}
これでコントラクト部分は完成です!
テストの作成
では次にテストを作成していきましょう!
以下のコマンドを実行してテストファイルを作成してください。
$ mkdir test/bank-test.js
bank-test.js
ファイルを作成したら、以下を貼り付けてください。
const { expect } = require("chai")
const { ethers } = require("hardhat")
// 署名を作成。
async function getPermitSignature(owner, contract, spender, value, deadline) {
const [nonce, name, version, chainId] = await Promise.all([
contract.nonces(owner.address),
contract.name(),
"1",
owner.getChainId(),
])
return ethers.utils.splitSignature(
await owner._signTypedData(
{
name,
version,
chainId,
verifyingContract: contract.address,
},
{
Permit: [
{
name: "owner",
type: "address",
},
{
name: "spender",
type: "address",
},
{
name: "value",
type: "uint256",
},
{
name: "nonce",
type: "uint256",
},
{
name: "deadline",
type: "uint256",
},
],
},
{
owner: owner.address,
spender,
value,
nonce,
deadline,
}
)
)
}
describe("ERC20Permit", function () {
it("ERC20 permit", async function () {
// アドレスを用意
// owner: ERC20トークンの所有者。
// contractOwner: Bankコントラクトをデプロイするアドレス。
// Admin: ERC20トークンを受け取り、ガス代を支払うアドレス。
[owner, contractOwner, admin] = await ethers.getSigners();
// Bankコントラクトをデプロイ。
const Bank = await ethers.getContractFactory("Bank", contractOwner)
const bank = await Bank.deploy("Cardene Token", "CARD")
await bank.deployed()
// 1000のERC20トークンを発行。
const amount = 1000
await bank.connect(contractOwner).mint(owner.address, amount)
// ERC20Permitの処理が有効な期間を指定。
// 今回は指定できる最大値を指定。
const deadline = ethers.constants.MaxUint256
// ownerアドレスによる署名を作成。
const { v, r, s } = await getPermitSignature(
owner,
bank,
admin.address,
amount,
deadline
)
// ERC20Permitを実行して、ownerアドレスからadminアドレスにERC20トークンを送る。
await bank.connect(admin).depositWithPermit(owner.address, admin.address, amount, deadline, v, r, s)
// adminアドレスにERC20トークンが送れているか確認。
expect(await bank.connect(admin).balanceOf(admin.address)).to.equal(amount)
})
})
処理の詳細はコメントでコード内に記述しています。
ざっくり処理の手順をまとめると以下になります。
手順
- ERC20トークンを
1000
個発行します。 owner
アドレスにより署名を作成。admin
アドレスがガス代を負担して、ERC20Permitを実行してowner
アドレスからadmin
アドレスにERC20トークンを送る。- 最後にしっかり
admin
アドレスにERC20トークンを送れているか確認。
ERC20Permitの実行
では前章で作成したコードをもとにテストを実行してみましょう!
以下のコマンドを実行してください。
$ npx hardhat compile
$ npx hardhat test
以下のように出力されていればテストは成功です!
ERC20Permit
✔ ERC20 permit (1507ms)
1 passing (2s)
コードは以下に置いてあります。
https://github.com/cardene777/smart-contract-test/tree/main/ERC20Permit
最後に
今回は「ERC20Permit」について解説してきました。
前提知識を最初に解説した後に「ERC20Permit」について解説したので理解しやすかったのではないでしょうか?
後半はコードも確認しながら解説してきたので、実装についても理解できていたら嬉しいです!
もし何か質問などがあれば以下のTwitterなどから連絡ください!
普段はSolidityやブロックチェーン、Web3についての情報発信をしています。
Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!
参考
https://ethereum.org/ja/developers/docs/transactions/
https://soliditydeveloper.com/erc20-permit
EIP-2612がYearn FinanceやOpenZeppelinに採用され普及が進む
https://soliditydeveloper.com/erc20-permit
すべての送金記録を並べるブロックチェーン|ブロックチェーンの基礎
【レッスン②】イーサリアムのアカウントとトランザクション構造
トランザクションレベルで理解する。イーサリアムの具体的な仕組みを解説
https://recruit.gmo.jp/engineer/jisedai/blog/gsn/