ERC20Permitについて
1. Overview
ガイドのターゲット
- ERC20のPermit関数とは何か、使うことのメリット
- Permit関数のコード解説
- EIP-712について
必要な知識
- Solidityの簡単な文法
- ECDSA(楕円曲線暗号)
- トランザクションの署名と承認の流れ
2. ERC20のPermit関数とは何か、使うことのメリット
Open Zepplein|docs
簡単に説明すると、ERC20のトークンを送信するには通常、transferFrom関数を呼び出す前にトランザクションでapprove関数を呼び出す必要があります。しかし、このpermit関数を用いれば、トランザクションではなくアカウントの秘密鍵で署名されたメッセージでapprove関数を呼び出すことができるのです。secp256k1署名(オフチェーン署名)
トランザクションを投げずにapprove関数を呼び出せることのメリットはユーザーがガス代を負担する必要が全くなくなるということです。transferFrom関数のトランザクションはDappsの方で呼ばれます。
ガスレス(Gas-Less)でUX向上させるーEthereum Gas Station Network(GSN)
こちらにガスレスがUXを向上させる説明がされています。秘密鍵を明かさずにガスの支払いだけ他の人に肩代わりする方法がメタトランザクションと呼ばれるものです。
イーサリアムERC-20メタトランザクションを理解する
UX向上の取組の最前線 – イーサリアムでのガスレストランザクションとオートメーション(前編)
こちらにpermit関数がメタトランザクションにおいて重要な関数であることを説明しています。permit関数以外にEIP-3009のtransferWithAuthorizationの説明もあります。
3. Permit関数のコード解説
OpenZeppelinのgithubより
ERC20Permitのコード
EIP-712のコード
ECDSA(楕円曲線暗号)のコード
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.0 (token/ERC20/extensions/draft-ERC20Permit.sol)
pragma solidity ^0.8.0;
import "./draft-IERC20Permit.sol"; // Interface
import "../ERC20.sol"; // ERC20Token
import "../../../utils/cryptography/draft-EIP712.sol"; // オフチェーン署名に含まれるコントラクトのメタデータのハッシュ値を管理する
import "../../../utils/cryptography/ECDSA.sol"; // 楕円曲線暗号のライブラリ、内部でecrecover関数を実行
import "../../../utils/Counters.sol"; // 安全に数を扱えるライブラリ、数を直接変更することができない。
/**
* @dev Implementation of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in
* https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].
*
* Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by
* presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't
* need to send a transaction, and thus is not required to hold Ether at all.
*
* _Available since v3.4._
*/
abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712 {
using Counters for Counters.Counter; // Counters.Counterはstruct
// アドレスのノンスが検証する際に必要
// address => struct
mapping(address => Counters.Counter) private _nonces;
// solhint-disable-next-line var-name-mixedcase
bytes32 private immutable _PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
/**
* @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`.
*
* It's a good idea to use the same `name` that is defined as the ERC20 token name.
*/
// EIP-712のconstructorを実行する
// ERC20Permitコントラクトの継承先でnameを指定する
// nameはトークン名で良い、独自の名前空間をつくる
constructor(string memory name) EIP712(name, "1") {}
/**
* @dev See {IERC20Permit-permit}.
*/
function permit(
address owner, // メッセージに署名したアドレス、approve関数を実行する
address spender, // approve先のアドレス
uint256 value, // approveする量
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));
// EIP712で管理しているメタデータのハッシュ値を含めてもう一度ハッシュ化します
bytes32 hash = _hashTypedDataV4(structHash);
// 検証用の変数のv,r,sを使ってアドレスを復元します
// 内部でecrecover関数を使用しています
address signer = ECDSA.recover(hash, v, r, s);
// 復元されたアドレスと引数のアドレスが一致すれば成功
require(signer == owner, "ERC20Permit: invalid signature");
_approve(owner, spender, value);
}
/**
* @dev See {IERC20Permit-nonces}.
*/
// アドレス毎のノンスを公開
function nonces(address owner) public view virtual override returns (uint256) {
return _nonces[owner].current();
}
/**
* @dev See {IERC20Permit-DOMAIN_SEPARATOR}.
*/
// solhint-disable-next-line func-name-mixedcase
// 外部からEIP712で管理されているメタ情報のハッシュ値を取得する
function DOMAIN_SEPARATOR() external view override returns (bytes32) {
return _domainSeparatorV4();
}
/**
* @dev "Consume a nonce": return the current value and increment.
*
* _Available since v4.1._
*/
// ノンスを更新し、更新前のノンスの値を返す
function _useNonce(address owner) internal virtual returns (uint256 current) {
Counters.Counter storage nonce = _nonces[owner];
current = nonce.current();
nonce.increment();
}
}
EIP-712では下記の5つの情報のハッシュ値を管理します。
bytes32 typeHash = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
return keccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid, address(this)));
Permit関数内で呼ばれていたこの関数ですが上記のハッシュ値とstructHashをECDSAに渡していますね。
function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) {
return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash);
}
ではECDSAのライブラリをみてみましょう。ECDSAのライブラリでは引数を下記のようにまとめて再度ハッシュ化しているだけです。
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
}
次はECDSAのライブラリで楕円曲線暗号を復元している関数です。s、vに誤りがないかを調べたのちecrecover関数を呼び出しているだけです。このecrecoverはsolidityが内部で持つ関数です。
function tryRecover(
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) internal pure returns (address, RecoverError) {
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return (address(0), RecoverError.InvalidSignatureS);
}
if (v != 27 && v != 28) {
return (address(0), RecoverError.InvalidSignatureV);
}
// If the signature is valid (and not malleable), return the signer address
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) {
return (address(0), RecoverError.InvalidSignature);
}
return (signer, RecoverError.NoError);
}
4. EIP-712について
上記のコードをみてみるとアドレスを検証する際にEIP-712が管理しているコントラクトのメタ情報を利用しています。なぜコントラクトのメタ情報が検証の際に必要なのでしょうか。
こちらに日本語でEIP-712について解説している記事があったのですが、なかなか理解できなかったので簡単にまとめます。まず、前提として、トランザクションには秘密鍵に署名されている情報と署名されていない情報があります。署名されている情報は呼び出す関数とその引数の情報(以下メッセージ)です。署名されていない情報は送り先のアドレスや送り主のアドレス、ガス代などです。つまり、送り先のアドレスは署名されていないので自由に変更できてしまうのです。そこで署名されている情報とまだ署名されていない、コントラクトのメタ情報をまとめてハッシュ化し、秘密鍵で署名をしているのです。その際に発行されるのがs、v、rです。ガスのリレイヤーはs、v、rを受け取ってトランザクションの送り先のコントラクトを調べます。この秘密鍵によって生成されるs、v、rはメッセージの内容によって変わるため公開しても問題ありません。コントラクトの内部でs、v、rとメッセージを検証することでownerのアドレスが一致することだけでなく、正しいコントラクトに送られたことも確認できるのです。
Discussion