[Bunzz Decipher] 『Permit2』コントラクトを理解しよう!
はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回はBunzzの新機能『DeCipher』を使用して、「Permit2」コントラクトを見てみようと思います。
『DeCipher』はAIを使用してコントラクトのドキュメントを自動生成してくれるサービスです。
詳しい使い方に関しては以下の記事を参考にしてください!
今回使用する『DeCipher』のリンクは以下になります。
Etherscanのリンクは以下になります。
概要
Permit2コントラクトは、Ethereumブロックチェーン上で安全で効率的なトークンの送受信を実現します。
目的
ユーザーが代理でトークンの送受信権限を許可できるようにすることです。
これにより、特定のアドレスに対してトークンの送信権限を委任し、各トランザクションごとに手動で承認する手間を省くことができます。
役割
-
Allowance管理
このコントラクトは、ユーザーが許可したAllowance
(トークンの最大送信量)を管理します。
これにより、ユーザーは他者に代理でトークンを送信する権限を制御できます。 -
署名の検証
SignatureVerificationライブラリを用いて、ユーザーが提供した署名が正当かどうかを確認します。
これにより、許可された個人のみがトークンの送受信を開始できます。 -
パーミットハッシュの生成
PermitHashライブラリを利用して、各Allowance
ごとに一意のハッシュを生成します。
これにより、トークンの送信権限が本物であるかどうかを検証できます。 -
トークンの送受信
ERC20.solライブラリを通じて、ERC20トークンの標準を組み込んでいます。
これにより、トークンを安全に他のアドレスに送受信することが可能です。 -
安全な送受信
SafeTransferLibライブラリを使用して、トークン送受信時のセキュリティチェックを行い、潜在的な脆弱性から保護します。 -
EIP712のサポート
EIP712.solコントラクトを導入することで、セキュアなメッセージ署名をサポートします。 -
エラーハンドリング
PermitErrors.solコントラクトは、異常な状況に対応するためのエラーコードとメッセージを提供し、ユーザーにわかりやすいエラー情報を提供します。 -
インターフェースの遵守
Permit2コントラクトは、他のコントラクトやシステムとの互換性を確保するため、さまざまなインターフェースに準拠しています。
使い方
Permit2コントラクトは、トークン所有者の代理でトークンの承認と送受信を行うためのスマートコントラクトです。
このコントラクトは、ERC20標準を実装しており、さらに承認と署名の検証に関する追加機能も備えています。
目標
トークン所有者が自身や承認されたアドレスでトークンの承認と送受信を安全かつ効率的に実行する方法を提供します。
所有者が手動で毎回承認する必要なく、より便利な方法でトークンの操作を行えるようにします。
利用方法
-
Permit2コントラクトのデプロイ
まず最初に、Permit2コントラクトをデプロイします。 -
approve関数の呼び出し
承認を与えるために、approve
関数を呼び出します。
これにより、支払人(spender
)が所有者の代理でトークンを送受信できる権限が設定されます。 -
transferFrom関数の呼び出し
承認されたアドレスが、transferFrom
関数を呼び出して、所有者のアカウントから別のアカウントにトークンを送信することができます。 -
permit関数の呼び出し
手軽な方法で承認を行うために、permit
関数を呼び出すことで、署名を使用して実行したアドレスが所有者の代理でトークンを送受信するための許可を与えることができます。 -
transferWithSignature関数の呼び出し
署名を使用して、transferWithSignature
関数を呼び出すことで、所有者のアカウントから別のアカウントにトークンを送信できます。
関数
-
approve(address spender, uint256 amount)
実行アドレスが指定した量のトークンを所有者の代理で送受信できるように承認します。 -
transferFrom(address sender, address recipient, uint256 amount)
承認されたアドレスが、所有者のアカウントから別のアカウントにトークンを送信します。 -
permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
署名を用いて実行アドレスが所有者の代理でトークンを送受信できるように許可します。 -
transferWithSignature(address sender, address recipient, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
署名を用いて、所有者のアカウントから別のアカウントにトークンを送信します。
イベント
-
Approval(address indexed owner, address indexed spender, uint256 value)
承認されたアドレスと許可量が設定または更新された時に発行されるイベント。 -
Transfer(address indexed from, address indexed to, uint256 value)
トークンが一つのアカウントから別のアカウントに送信された時に発行されるイベント。 -
Permit(address indexed owner, address indexed spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
署名を用いて支払人が所有者の代理でトークンを送受信できるように許可された時に発行されるイベント。 -
TransferWithSignature(address indexed sender, address indexed recipient, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
署名を用いて、トークンが所有者のアカウントから別のアカウントに送信された時に発行されるイベント。
関連EIP/ERC
-
ERC20
- The Permit2 contract implements the ERC20 standard for fungible tokens.
-
ERC721
- The Permit2 contract uses EIP-712 for signature verification.
-
ERC1271
- The Permit2 contract implements the IERC1271 interface for contract signature verification.
パラメーター
なし。
コントラクト
AllowanceTransfer
allowance
allowance
mapping(address => mapping(address => mapping(address => PackedAllowance))) public allowance;
概要
ユーザーを実行者アドレスにマッピングし、トークンの承認に関する情報を格納するマッピング配列。
詳細
このマッピングは3階層になっており、最初のキーで所有者のアドレス、2番目のキーでトークンのアドレス、3番目のキーで実行者アドレスを指定します。
これにより、各所有者が持つトークンに対する各実行者アドレスの承認情報を取得できます。PackedAllowance
は、許可された量、承認の有効期限、およびノンス(nonce)の情報を詰め込んだ構造体です。
パラメータ
-
owner
- トークン所有者のアドレス。
-
token
- 承認対象のトークンのアドレス。
-
spender
- 承認を持つアドレス。
-
PackedAllowance
- 承認情報が詰め込まれた構造体。
- 許可されたトークン量、承認の有効期限、およびノンス(nonce)が格納されています。
approve
approve
function approve(address token, address spender, uint160 amount, uint48 expiration) external {
PackedAllowance storage allowed = allowance[msg.sender][token][spender];
allowed.updateAmountAndExpiration(amount, expiration);
emit Approval(msg.sender, token, spender, amount, expiration);
}
概要
トークンの支払いに対する承認を設定する関数。
引数
-
token
- 承認対象のトークンのアドレス。
-
spender
- 承認を受けるアドレス。
-
amount
- 承認するトークンの数量。
-
expiration
- 承認の有効期限。
戻り値
なし。
permit
permit
function permit(address owner, PermitSingle memory permitSingle, bytes calldata signature) external {
if (block.timestamp > permitSingle.sigDeadline) revert SignatureExpired(permitSingle.sigDeadline);
// Verify the signer address from the signature.
signature.verify(_hashTypedData(permitSingle.hash()), owner);
_updateApproval(permitSingle.details, owner, permitSingle.spender);
}
概要
単一のトークン承認を行う関数。
引数
-
owner
- 承認を行うトークンの所有者のアドレス。
-
permitSingle
- 承認情報を格納する構造体。
-
signature
- 署名データ。
permit
permit
function permit(address owner, PermitBatch memory permitBatch, bytes calldata signature) external {
if (block.timestamp > permitBatch.sigDeadline) revert SignatureExpired(permitBatch.sigDeadline);
// Verify the signer address from the signature.
signature.verify(_hashTypedData(permitBatch.hash()), owner);
address spender = permitBatch.spender;
unchecked {
uint256 length = permitBatch.details.length;
for (uint256 i = 0; i < length; ++i) {
_updateApproval(permitBatch.details[i], owner, spender);
}
}
}
概要
複数のトークン承認を一括で行う関数。
引数
-
owner
- 承認を行うトークンの所有者のアドレス。
-
permitBatch
- 承認情報を格納する構造体の配列。
-
signature
- 署名データ。
transferFrom
transferFrom
function transferFrom(address from, address to, uint160 amount, address token) external {
_transfer(from, to, amount, token);
}
概要
トークンを指定したアドレスに送る関数。
引数
-
from
- トークンの送付先アドレス。
-
to
- トークンの送付元アドレス。
-
amount
- 送付するトークンの数量。
-
token
- 送付するトークンのアドレス。
戻り値
なし。
transferFrom
transferFrom
function transferFrom(AllowanceTransferDetails[] calldata transferDetails) external {
unchecked {
uint256 length = transferDetails.length;
for (uint256 i = 0; i < length; ++i) {
AllowanceTransferDetails memory transferDetail = transferDetails[i];
_transfer(transferDetail.from, transferDetail.to, transferDetail.amount, transferDetail.token);
}
}
}
概要
複数のトークンの送付を一括で行います。
詳細
-
transferDetails
- トークン送付情報を格納する構造体の配列。
引数
-
transferDetails
- トークン送付情報を格納する構造体の配列。
戻り値
なし。
_transfer
_transfer
function _transfer(address from, address to, uint160 amount, address token) private {
PackedAllowance storage allowed = allowance[from][token][msg.sender];
if (block.timestamp > allowed.expiration) revert AllowanceExpired(allowed.expiration);
uint256 maxAmount = allowed.amount;
if (maxAmount != type(uint160).max) {
if (amount > maxAmount) {
revert InsufficientAllowance(maxAmount);
} else {
unchecked {
allowed.amount = uint160(maxAmount) - amount;
}
}
}
// Transfer the tokens from the from address to the recipient.
ERC20(token).safeTransferFrom(from, to, amount);
}
概要
トークンの送付を実行する関数。
引数
-
from
- トークンを送付するアドレス。
-
to
- トークンを受け取るアドレス。
-
amount
- 送付するトークンの数量。
-
token
- 送付するトークンのアドレス。
lockdown
lockdown
function lockdown(TokenSpenderPair[] calldata approvals) external {
address owner = msg.sender;
// Revoke allowances for each pair of spenders and tokens.
unchecked {
uint256 length = approvals.length;
for (uint256 i = 0; i < length; ++i) {
address token = approvals[i].token;
address spender = approvals[i].spender;
allowance[owner][token][spender].amount = 0;
emit Lockdown(owner, token, spender);
}
}
}
概要
複数の承認を取り消してトークンのロックダウンを行う関数。
詳細
特定のトークンとspender
に対する承認を取り消します。
承認情報の量をゼロに設定し、Lockdown
イベントを発行します。
引数
-
approvals
- 承認を取り消すためのトークンと
spender
のペアの配列。
- 承認を取り消すためのトークンと
invalidateNonces
invalidateNonces
function invalidateNonces(address token, address spender, uint48 newNonce) external {
uint48 oldNonce = allowance[msg.sender][token][spender].nonce;
if (newNonce <= oldNonce) revert InvalidNonce();
// Limit the amount of nonces that can be invalidated in one transaction.
unchecked {
uint48 delta = newNonce - oldNonce;
if (delta > type(uint16).max) revert ExcessiveInvalidation();
}
allowance[msg.sender][token][spender].nonce = newNonce;
emit NonceInvalidation(msg.sender, token, spender, newNonce, oldNonce);
}
概要
nonce
(トークン承認の識別子)を無効化して更新する関数。
詳細
関数は、新しいnonce
が古いnonce
よりも大きいことを確認し、一定の条件を満たしてからnonce
を更新します。
また、関連するイベントを発行します。
引数
-
token
- トークンのアドレス。
-
spender
- 承認を受けるアドレス。
-
newNonce
- 新しい
nonce
の値。
- 新しい
戻り値
なし
_updateApproval
_updateApproval
function _updateApproval(PermitDetails memory details, address owner, address spender) private {
uint48 nonce = details.nonce;
address token = details.token;
uint160 amount = details.amount;
uint48 expiration = details.expiration;
PackedAllowance storage allowed = allowance[owner][token][spender];
if (allowed.nonce != nonce) revert InvalidNonce();
allowed.updateAll(amount, expiration, nonce);
emit Permit(owner, token, spender, amount, expiration, nonce);
}
概要
承認情報を更新する関数。
詳細
署名されたnonce
が現在のnonce
と等しいか確認し、nonce
の値を更新します。
Permit
イベントを発行します。
引数
-
details
- 承認の詳細情報を格納した構造体。
-
owner
- 承認許可の所有者のアドレス。
-
spender
- 承認を受けるアドレス。
SignatureTransfer
nonceBitmap
nonceBitmapnonceBitmap
mapping(address => mapping(uint256 => uint256)) public nonceBitmap;
概要
アドレスとnonce
のペアに関連付けられたビットマップを格納するマッピング配列。
トランザクションのノンスを管理するために使用されます。
パラメータ
-
address
- アドレスをキーとして使用し、特定のアドレスに関連するノンスの情報を格納します。
-
uint256
- ノンスを表す整数値。
- 各アドレスごとに複数のノンスが存在する場合、それぞれのノンスに対応するビットマップが格納されます。
-
uint256
- ノンスの状態を表すビットマップ。
-ビットの位置がトランザクションのノンスに対応し、ビットがセットされている場合はそのノンスが使用済みであることを示します。
- ノンスの状態を表すビットマップ。
permitTransferFrom
permitTransferFrom
function permitTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes calldata signature
) external {
_permitTransferFrom(permit, transferDetails, owner, permit.hash(), signature);
}
概要
署名付きのメッセージを使用してトークンを転送する関数。
詳細
署名されたメッセージを受け取り、指定された条件に基づいてトークンの転送を実行します。
引数
-
permit
- 許可メッセージに関する情報を含む構造体。
-
transferDetails
- 転送詳細を含むデータ。
-
owner
- トークンの所有者のアドレス。
-
signature
- 署名データ。
permitWitnessTransferFrom
permitWitnessTransferFrom
function permitWitnessTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes32 witness,
string calldata witnessTypeString,
bytes calldata signature
) external {
_permitTransferFrom(
permit, transferDetails, owner, permit.hashWithWitness(witness, witnessTypeString), signature
);
}
概要
署名アドレスと共に署名されたメッセージを使用してトークンを転送する関数。
引数
-
permit
- 許可メッセージに関する情報を含む構造体。
-
transferDetails
- 転送詳細を含むデータ。
-
owner
- トークンの所有者のアドレス。
-
witness
- 署名者のアドレスまたはハッシュ。
-
witnessTypeString
- 署名者の種類を示す文字列。
-
signature
- 署名データ。
_permitTransferFrom
_permitTransferFrom
function _permitTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes32 dataHash,
bytes calldata signature
) private {
uint256 requestedAmount = transferDetails.requestedAmount;
if (block.timestamp > permit.deadline) revert SignatureExpired(permit.deadline);
if (requestedAmount > permit.permitted.amount) revert InvalidAmount(permit.permitted.amount);
_useUnorderedNonce(owner, permit.nonce);
signature.verify(_hashTypedData(dataHash), owner);
ERC20(permit.permitted.token).safeTransferFrom(owner, transferDetails.to, requestedAmount);
}
概要
実際のトークン転送処理を行う関数。
詳細
指定された許可データに基づいて、トークンの転送が許可されたかどうかを確認し、署名を検証してトークンを転送します。
引数
-
permit
- 許可メッセージに関する情報を含む構造体。
-
transferDetails
- 転送詳細を含むデータ。
-
owner
- トークンの所有者のアドレス。
-
dataHash
- 署名の検証に使用するデータハッシュ。
-
signature
- 署名データ。
permitTransferFrom
permitTransferFrom
function permitTransferFrom(
PermitBatchTransferFrom memory permit,
SignatureTransferDetails[] calldata transferDetails,
address owner,
bytes calldata signature
) external {
_permitTransferFrom(permit, transferDetails, owner, permit.hash(), signature);
}
概要
署名付きの許可メッセージを使用して複数のトークンを一括で転送する関数。
詳細
複数のトークンに対する一括転送を行います。
複数のトークンの許可データと署名を受け取り、指定された条件に基づいてトークンの一括転送を実行します。
トークンの所有者が署名し、転送先と転送量の詳細も指定します。
引数
-
permit
- 許可メッセージに関する情報を含む構造体。
-
transferDetails
- 複数の転送詳細を含むデータの配列。
-
owner
- トークンの所有者のアドレス。
-
signature
- 署名データ。
permitWitnessTransferFrom
permitWitnessTransferFrom
function permitWitnessTransferFrom(
PermitBatchTransferFrom memory permit,
SignatureTransferDetails[] calldata transferDetails,
address owner,
bytes32 witness,
string calldata witnessTypeString,
bytes calldata signature
) external {
_permitTransferFrom(
permit, transferDetails, owner, permit.hashWithWitness(witness, witnessTypeString), signature
);
}
概要
署名者と共に署名された許可メッセージを使用して複数のトークンを一括で転送する関数。
詳細
複数のトークンに対する一括転送を行います。
複数のトークンの許可データと署名、および証人の情報を受け取り、指定された条件に基づいてトークンの一括転送を実行します。
トークンの所有者が署名し、転送先と転送量の詳細、証人情報も指定します。
引数
-
permit
- 許可メッセージに関する情報を含む構造体。
-
transferDetails
- 複数の転送詳細を含むデータの配列。
-
owner
- トークンの所有者のアドレス。
-
witness
- 署名者のアドレスまたはハッシュ。
-
witnessTypeString
- 署名者の種類を示す文字列。
-
signature
- 署名データ。
_permitTransferFrom
_permitTransferFrom
function _permitTransferFrom(
PermitBatchTransferFrom memory permit,
SignatureTransferDetails[] calldata transferDetails,
address owner,
bytes32 dataHash,
bytes calldata signature
) private {
uint256 numPermitted = permit.permitted.length;
if (block.timestamp > permit.deadline) revert SignatureExpired(permit.deadline);
if (numPermitted != transferDetails.length) revert LengthMismatch();
_useUnorderedNonce(owner, permit.nonce);
signature.verify(_hashTypedData(dataHash), owner);
unchecked {
for (uint256 i = 0; i < numPermitted; ++i) {
TokenPermissions memory permitted = permit.permitted[i];
uint256 requestedAmount = transferDetails[i].requestedAmount;
if (requestedAmount > permitted.amount) revert InvalidAmount(permitted.amount);
if (requestedAmount != 0) {
// allow spender to specify which of the permitted tokens should be transferred
ERC20(permitted.token).safeTransferFrom(owner, transferDetails[i].to, requestedAmount);
}
}
}
}
概要
送付許可されたトークンの送付処理を行う関数。
詳細
複数のトークンに対する一括の許可トークン転送を実行します。
指定された許可データと署名を使用して、トークンの所有者が許可したトークン転送を実行します。
一括で転送されるトークンの詳細情報も指定します。
引数
-
permit
- 許可メッセージに関する情報を含む構造体。
-
transferDetails
- 複数の転送詳細を含むデータの配列。
-
owner
- トークンの所有者のアドレス。
-
dataHash
- 署名の検証に使用するデータハッシュ。
-
signature
- 署名データ。
invalidateUnorderedNonces
invalidateUnorderedNonces
function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external {
nonceBitmap[msg.sender][wordPos] |= mask;
emit UnorderedNonceInvalidation(msg.sender, wordPos, mask);
}
概要
無順のNonceを無効化する関数。
詳細
指定されたワード位置とマスクを使用して無順のNonceを無効化します。
これにより、Nonceが再利用されないようにします。
引数
-
wordPos
- ワード位置。
-
mask
- マスク。
bitmapPositions
bitmapPositions
function bitmapPositions(uint256 nonce) private pure returns (uint256 wordPos, uint256 bitPos) {
wordPos = uint248(nonce >> 8);
bitPos = uint8(nonce);
}
概要
Nonceのビットマップ位置を取得する関数。
詳細
指定されたNonceのビットマップ位置(ワード位置とビット位置)を計算して返します。
Nonceの上位248
ビットがビットマップのインデックス、下位8
ビットがビット位置として使用されます。
引数
-
nonce
- Nonce値。
戻り値
-
wordPos
- ワード位置。
-
bitPos
- ビット位置。
_useUnorderedNonce
_useUnorderedNonce
unction _useUnorderedNonce(address from, uint256 nonce) internal {
(uint256 wordPos, uint256 bitPos) = bitmapPositions(nonce);
uint256 bit = 1 << bitPos;
uint256 flipped = nonceBitmap[from][wordPos] ^= bit;
if (flipped & bit == 0) revert InvalidNonce();
}
概要
無順のNonceを使用し、対応するビットをビットマップ内に設定する関数。
詳細
指定されたアドレスとNonceを使用して、対応するビットをビットマップ内で設定します。
Nonceがすでに使用されている場合は例外がスローされます。
引数
-
from
- アドレス。
-
nonce
- Nonce値。
イベント・エラー
AllowanceExpired
AllowanceExpired
error AllowanceExpired(uint256 deadline);
概要
トークンの許可量の有効期限が切れた場合に呼ばれるエラー。
詳細
トークンの所有者が設定した許可量の有効期限が現在のタイムスタンプを過ぎている場合に呼ばれます。
これにより、期限切れの許可が使用されないようになります。
パラメータ
-
deadline
- 許可量の有効期限のタイムスタンプ。
InsufficientAllowance
InsufficientAllowance
error InsufficientAllowance(uint256 amount);
概要
トークンの許可量が不足している場合に呼ばれるエラー。
詳細
トークンの所有者が設定した許可量を超えてトークンが使用された場合に呼ばれます。
これにより、許可されたトークンの範囲を超えてのトークン使用が防止されます。
パラメータ
-
amount
- 許可量の最大値。
ExcessiveInvalidation
ExcessiveInvalidation
error ExcessiveInvalidation();
概要
無効化される無順のNonceの数が過剰である場合に呼ばれるエラー。
詳細
無効化される無順のNonceの数が許容範囲を超えている場合に呼ばれます。
無効化の過剰使用を防ぐために設定されています。
NonceInvalidation
NonceInvalidation
event NonceInvalidation(
address indexed owner, address indexed token, address indexed spender, uint48 newNonce, uint48 oldNonce
);
概要
所有者が無順のNonceを無効化した時に発行されるイベント。
詳細
所有者が無順のNonceを無効化した時に発行され、新しいNonceと古いNonceの情報を含みます。
これにより、Nonceの無効化が確認できます。
パラメータ
-
owner
- トークンの所有者のアドレス。
-
token
- トークンのアドレス。
-
spender
- トークンの送付を許可されたアドレス。
-
newNonce
- 新しいNonceの値。
-
oldNonce
- 古いNonceの値。
Approval
Approval
event Approval(
address indexed owner, address indexed token, address indexed spender, uint160 amount, uint48 expiration
);
概要
所有者がトークンの許可量と有効期限を設定した時に発行されるイベント。
詳細
所有者がトークンの許可量と有効期限を設定した時に発行され、設定された許可量と有効期限の情報を含みます。
これにより、他のアドレスがトークンをどれだけ使用できるかが示されます。
パラメータ
-
owner
- トークンの所有者のアドレス。
-
token
- トークンのアドレス。
-
spender
- トークンの送付を許可されたアドレス。
-
amount
- 許可されたトークンの量。
-
expiration
- 有効期限のタイムスタンプ。
Permit
Permit
event Permit(
address indexed owner,
address indexed token,
address indexed spender,
uint160 amount,
uint48 expiration,
uint48 nonce
);
概要
所有者が許可メッセージの署名を使用してトークンの許可量と有効期限を設定した時に発行されるイベント。
詳細
所有者が許可メッセージの署名を使用してトークンの許可量と有効期限を設定した時に発行され、設定された許可量、有効期限、およびNonceの情報を含みます。
これにより、他のアドレスがトークンをどれだけ使用できるかが示されます。
パラメータ
-
owner
- トークンの所有者のアドレス。
-
token
- トークンのアドレス。
-
spender
- トークンの送付を許可されたアドレス。
-
amount
- 許可されたトークンの量。
-
expiration
- 有効期限のタイムスタンプ。
-
nonce
- Nonceの値。
Lockdown
Lockdown
event Lockdown(address indexed owner, address token, address spender);
概要
所有者がトークンの許可量を0
に設定した時に発行されるイベント。
詳細
トークンの使用が完全に無効化されることを示します。
パラメータ
-
owner
- トークンの所有者のアドレス。
-
token
- トークンのアドレス。
-
spender
- トークンの送付を許可されたアドレス。
コード
Permit2.sol
Permit2
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import {SignatureTransfer} from "./SignatureTransfer.sol";
import {AllowanceTransfer} from "./AllowanceTransfer.sol";
/// @notice Permit2 handles signature-based transfers in SignatureTransfer and allowance-based transfers in AllowanceTransfer.
/// @dev Users must approve Permit2 before calling any of the transfer functions.
contract Permit2 is SignatureTransfer, AllowanceTransfer {
// Permit2 unifies the two contracts so users have maximal flexibility with their approval.
}
AllowanceTransfer.sol
AllowanceTransfer.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import {ERC20} from "solmate/tokens/ERC20.sol";
import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol";
import {PermitHash} from "./libraries/PermitHash.sol";
import {SignatureVerification} from "./libraries/SignatureVerification.sol";
import {EIP712} from "./EIP712.sol";
import {IAllowanceTransfer} from "../src/interfaces/IAllowanceTransfer.sol";
import {SignatureExpired, InvalidNonce} from "./PermitErrors.sol";
import {Allowance} from "./libraries/Allowance.sol";
contract AllowanceTransfer is IAllowanceTransfer, EIP712 {
using SignatureVerification for bytes;
using SafeTransferLib for ERC20;
using PermitHash for PermitSingle;
using PermitHash for PermitBatch;
using Allowance for PackedAllowance;
/// @notice Maps users to tokens to spender addresses and information about the approval on the token
/// @dev Indexed in the order of token owner address, token address, spender address
/// @dev The stored word saves the allowed amount, expiration on the allowance, and nonce
mapping(address => mapping(address => mapping(address => PackedAllowance))) public allowance;
/// @inheritdoc IAllowanceTransfer
function approve(address token, address spender, uint160 amount, uint48 expiration) external {
PackedAllowance storage allowed = allowance[msg.sender][token][spender];
allowed.updateAmountAndExpiration(amount, expiration);
emit Approval(msg.sender, token, spender, amount, expiration);
}
/// @inheritdoc IAllowanceTransfer
function permit(address owner, PermitSingle memory permitSingle, bytes calldata signature) external {
if (block.timestamp > permitSingle.sigDeadline) revert SignatureExpired(permitSingle.sigDeadline);
// Verify the signer address from the signature.
signature.verify(_hashTypedData(permitSingle.hash()), owner);
_updateApproval(permitSingle.details, owner, permitSingle.spender);
}
/// @inheritdoc IAllowanceTransfer
function permit(address owner, PermitBatch memory permitBatch, bytes calldata signature) external {
if (block.timestamp > permitBatch.sigDeadline) revert SignatureExpired(permitBatch.sigDeadline);
// Verify the signer address from the signature.
signature.verify(_hashTypedData(permitBatch.hash()), owner);
address spender = permitBatch.spender;
unchecked {
uint256 length = permitBatch.details.length;
for (uint256 i = 0; i < length; ++i) {
_updateApproval(permitBatch.details[i], owner, spender);
}
}
}
/// @inheritdoc IAllowanceTransfer
function transferFrom(address from, address to, uint160 amount, address token) external {
_transfer(from, to, amount, token);
}
/// @inheritdoc IAllowanceTransfer
function transferFrom(AllowanceTransferDetails[] calldata transferDetails) external {
unchecked {
uint256 length = transferDetails.length;
for (uint256 i = 0; i < length; ++i) {
AllowanceTransferDetails memory transferDetail = transferDetails[i];
_transfer(transferDetail.from, transferDetail.to, transferDetail.amount, transferDetail.token);
}
}
}
/// @notice Internal function for transferring tokens using stored allowances
/// @dev Will fail if the allowed timeframe has passed
function _transfer(address from, address to, uint160 amount, address token) private {
PackedAllowance storage allowed = allowance[from][token][msg.sender];
if (block.timestamp > allowed.expiration) revert AllowanceExpired(allowed.expiration);
uint256 maxAmount = allowed.amount;
if (maxAmount != type(uint160).max) {
if (amount > maxAmount) {
revert InsufficientAllowance(maxAmount);
} else {
unchecked {
allowed.amount = uint160(maxAmount) - amount;
}
}
}
// Transfer the tokens from the from address to the recipient.
ERC20(token).safeTransferFrom(from, to, amount);
}
/// @inheritdoc IAllowanceTransfer
function lockdown(TokenSpenderPair[] calldata approvals) external {
address owner = msg.sender;
// Revoke allowances for each pair of spenders and tokens.
unchecked {
uint256 length = approvals.length;
for (uint256 i = 0; i < length; ++i) {
address token = approvals[i].token;
address spender = approvals[i].spender;
allowance[owner][token][spender].amount = 0;
emit Lockdown(owner, token, spender);
}
}
}
/// @inheritdoc IAllowanceTransfer
function invalidateNonces(address token, address spender, uint48 newNonce) external {
uint48 oldNonce = allowance[msg.sender][token][spender].nonce;
if (newNonce <= oldNonce) revert InvalidNonce();
// Limit the amount of nonces that can be invalidated in one transaction.
unchecked {
uint48 delta = newNonce - oldNonce;
if (delta > type(uint16).max) revert ExcessiveInvalidation();
}
allowance[msg.sender][token][spender].nonce = newNonce;
emit NonceInvalidation(msg.sender, token, spender, newNonce, oldNonce);
}
/// @notice Sets the new values for amount, expiration, and nonce.
/// @dev Will check that the signed nonce is equal to the current nonce and then incrememnt the nonce value by 1.
/// @dev Emits a Permit event.
function _updateApproval(PermitDetails memory details, address owner, address spender) private {
uint48 nonce = details.nonce;
address token = details.token;
uint160 amount = details.amount;
uint48 expiration = details.expiration;
PackedAllowance storage allowed = allowance[owner][token][spender];
if (allowed.nonce != nonce) revert InvalidNonce();
allowed.updateAll(amount, expiration, nonce);
emit Permit(owner, token, spender, amount, expiration, nonce);
}
}
SignatureTransfer.sol
SignatureTransfer.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import {ISignatureTransfer} from "./interfaces/ISignatureTransfer.sol";
import {SignatureExpired, InvalidNonce} from "./PermitErrors.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol";
import {SignatureVerification} from "./libraries/SignatureVerification.sol";
import {PermitHash} from "./libraries/PermitHash.sol";
import {EIP712} from "./EIP712.sol";
contract SignatureTransfer is ISignatureTransfer, EIP712 {
using SignatureVerification for bytes;
using SafeTransferLib for ERC20;
using PermitHash for PermitTransferFrom;
using PermitHash for PermitBatchTransferFrom;
/// @inheritdoc ISignatureTransfer
mapping(address => mapping(uint256 => uint256)) public nonceBitmap;
/// @inheritdoc ISignatureTransfer
function permitTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes calldata signature
) external {
_permitTransferFrom(permit, transferDetails, owner, permit.hash(), signature);
}
/// @inheritdoc ISignatureTransfer
function permitWitnessTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes32 witness,
string calldata witnessTypeString,
bytes calldata signature
) external {
_permitTransferFrom(
permit, transferDetails, owner, permit.hashWithWitness(witness, witnessTypeString), signature
);
}
/// @notice Transfers a token using a signed permit message.
/// @dev If to is the zero address, the tokens are sent to the spender.
/// @param permit The permit data signed over by the owner
/// @param dataHash The EIP-712 hash of permit data to include when checking signature
/// @param owner The owner of the tokens to transfer
/// @param transferDetails The spender's requested transfer details for the permitted token
/// @param signature The signature to verify
function _permitTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes32 dataHash,
bytes calldata signature
) private {
uint256 requestedAmount = transferDetails.requestedAmount;
if (block.timestamp > permit.deadline) revert SignatureExpired(permit.deadline);
if (requestedAmount > permit.permitted.amount) revert InvalidAmount(permit.permitted.amount);
_useUnorderedNonce(owner, permit.nonce);
signature.verify(_hashTypedData(dataHash), owner);
ERC20(permit.permitted.token).safeTransferFrom(owner, transferDetails.to, requestedAmount);
}
/// @inheritdoc ISignatureTransfer
function permitTransferFrom(
PermitBatchTransferFrom memory permit,
SignatureTransferDetails[] calldata transferDetails,
address owner,
bytes calldata signature
) external {
_permitTransferFrom(permit, transferDetails, owner, permit.hash(), signature);
}
/// @inheritdoc ISignatureTransfer
function permitWitnessTransferFrom(
PermitBatchTransferFrom memory permit,
SignatureTransferDetails[] calldata transferDetails,
address owner,
bytes32 witness,
string calldata witnessTypeString,
bytes calldata signature
) external {
_permitTransferFrom(
permit, transferDetails, owner, permit.hashWithWitness(witness, witnessTypeString), signature
);
}
/// @notice Transfers tokens using a signed permit messages
/// @dev If to is the zero address, the tokens are sent to the spender
/// @param permit The permit data signed over by the owner
/// @param dataHash The EIP-712 hash of permit data to include when checking signature
/// @param owner The owner of the tokens to transfer
/// @param signature The signature to verify
function _permitTransferFrom(
PermitBatchTransferFrom memory permit,
SignatureTransferDetails[] calldata transferDetails,
address owner,
bytes32 dataHash,
bytes calldata signature
) private {
uint256 numPermitted = permit.permitted.length;
if (block.timestamp > permit.deadline) revert SignatureExpired(permit.deadline);
if (numPermitted != transferDetails.length) revert LengthMismatch();
_useUnorderedNonce(owner, permit.nonce);
signature.verify(_hashTypedData(dataHash), owner);
unchecked {
for (uint256 i = 0; i < numPermitted; ++i) {
TokenPermissions memory permitted = permit.permitted[i];
uint256 requestedAmount = transferDetails[i].requestedAmount;
if (requestedAmount > permitted.amount) revert InvalidAmount(permitted.amount);
if (requestedAmount != 0) {
// allow spender to specify which of the permitted tokens should be transferred
ERC20(permitted.token).safeTransferFrom(owner, transferDetails[i].to, requestedAmount);
}
}
}
}
/// @inheritdoc ISignatureTransfer
function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external {
nonceBitmap[msg.sender][wordPos] |= mask;
emit UnorderedNonceInvalidation(msg.sender, wordPos, mask);
}
/// @notice Returns the index of the bitmap and the bit position within the bitmap. Used for unordered nonces
/// @param nonce The nonce to get the associated word and bit positions
/// @return wordPos The word position or index into the nonceBitmap
/// @return bitPos The bit position
/// @dev The first 248 bits of the nonce value is the index of the desired bitmap
/// @dev The last 8 bits of the nonce value is the position of the bit in the bitmap
function bitmapPositions(uint256 nonce) private pure returns (uint256 wordPos, uint256 bitPos) {
wordPos = uint248(nonce >> 8);
bitPos = uint8(nonce);
}
/// @notice Checks whether a nonce is taken and sets the bit at the bit position in the bitmap at the word position
/// @param from The address to use the nonce at
/// @param nonce The nonce to spend
function _useUnorderedNonce(address from, uint256 nonce) internal {
(uint256 wordPos, uint256 bitPos) = bitmapPositions(nonce);
uint256 bit = 1 << bitPos;
uint256 flipped = nonceBitmap[from][wordPos] ^= bit;
if (flipped & bit == 0) revert InvalidNonce();
}
}
EIP712.sol
EIP712.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
/// @notice EIP712 helpers for permit2
/// @dev Maintains cross-chain replay protection in the event of a fork
/// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/EIP712.sol
contract EIP712 {
// Cache the domain separator as an immutable value, but also store the chain id that it
// corresponds to, in order to invalidate the cached domain separator if the chain id changes.
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
uint256 private immutable _CACHED_CHAIN_ID;
bytes32 private constant _HASHED_NAME = keccak256("Permit2");
bytes32 private constant _TYPE_HASH =
keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)");
constructor() {
_CACHED_CHAIN_ID = block.chainid;
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME);
}
/// @notice Returns the domain separator for the current chain.
/// @dev Uses cached version if chainid and address are unchanged from construction.
function DOMAIN_SEPARATOR() public view returns (bytes32) {
return block.chainid == _CACHED_CHAIN_ID
? _CACHED_DOMAIN_SEPARATOR
: _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME);
}
/// @notice Builds a domain separator using the current chainId and contract address.
function _buildDomainSeparator(bytes32 typeHash, bytes32 nameHash) private view returns (bytes32) {
return keccak256(abi.encode(typeHash, nameHash, block.chainid, address(this)));
}
/// @notice Creates an EIP-712 typed data hash
function _hashTypedData(bytes32 dataHash) internal view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), dataHash));
}
}
Allowance
Allowance
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import {IAllowanceTransfer} from "../interfaces/IAllowanceTransfer.sol";
library Allowance {
// note if the expiration passed is 0, then it the approval set to the block.timestamp
uint256 private constant BLOCK_TIMESTAMP_EXPIRATION = 0;
/// @notice Sets the allowed amount, expiry, and nonce of the spender's permissions on owner's token.
/// @dev Nonce is incremented.
/// @dev If the inputted expiration is 0, the stored expiration is set to block.timestamp
function updateAll(
IAllowanceTransfer.PackedAllowance storage allowed,
uint160 amount,
uint48 expiration,
uint48 nonce
) internal {
uint48 storedNonce;
unchecked {
storedNonce = nonce + 1;
}
uint48 storedExpiration = expiration == BLOCK_TIMESTAMP_EXPIRATION ? uint48(block.timestamp) : expiration;
uint256 word = pack(amount, storedExpiration, storedNonce);
assembly {
sstore(allowed.slot, word)
}
}
/// @notice Sets the allowed amount and expiry of the spender's permissions on owner's token.
/// @dev Nonce does not need to be incremented.
function updateAmountAndExpiration(
IAllowanceTransfer.PackedAllowance storage allowed,
uint160 amount,
uint48 expiration
) internal {
// If the inputted expiration is 0, the allowance only lasts the duration of the block.
allowed.expiration = expiration == 0 ? uint48(block.timestamp) : expiration;
allowed.amount = amount;
}
/// @notice Computes the packed slot of the amount, expiration, and nonce that make up PackedAllowance
function pack(uint160 amount, uint48 expiration, uint48 nonce) internal pure returns (uint256 word) {
word = (uint256(nonce) << 208) | uint256(expiration) << 160 | amount;
}
}
PermitHash
PermitHash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import {IAllowanceTransfer} from "../interfaces/IAllowanceTransfer.sol";
import {ISignatureTransfer} from "../interfaces/ISignatureTransfer.sol";
library PermitHash {
bytes32 public constant _PERMIT_DETAILS_TYPEHASH =
keccak256("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)");
bytes32 public constant _PERMIT_SINGLE_TYPEHASH = keccak256(
"PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
);
bytes32 public constant _PERMIT_BATCH_TYPEHASH = keccak256(
"PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
);
bytes32 public constant _TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)");
bytes32 public constant _PERMIT_TRANSFER_FROM_TYPEHASH = keccak256(
"PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)"
);
bytes32 public constant _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH = keccak256(
"PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)"
);
string public constant _TOKEN_PERMISSIONS_TYPESTRING = "TokenPermissions(address token,uint256 amount)";
string public constant _PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB =
"PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,";
string public constant _PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB =
"PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,";
function hash(IAllowanceTransfer.PermitSingle memory permitSingle) internal pure returns (bytes32) {
bytes32 permitHash = _hashPermitDetails(permitSingle.details);
return
keccak256(abi.encode(_PERMIT_SINGLE_TYPEHASH, permitHash, permitSingle.spender, permitSingle.sigDeadline));
}
function hash(IAllowanceTransfer.PermitBatch memory permitBatch) internal pure returns (bytes32) {
uint256 numPermits = permitBatch.details.length;
bytes32[] memory permitHashes = new bytes32[](numPermits);
for (uint256 i = 0; i < numPermits; ++i) {
permitHashes[i] = _hashPermitDetails(permitBatch.details[i]);
}
return keccak256(
abi.encode(
_PERMIT_BATCH_TYPEHASH,
keccak256(abi.encodePacked(permitHashes)),
permitBatch.spender,
permitBatch.sigDeadline
)
);
}
function hash(ISignatureTransfer.PermitTransferFrom memory permit) internal view returns (bytes32) {
bytes32 tokenPermissionsHash = _hashTokenPermissions(permit.permitted);
return keccak256(
abi.encode(_PERMIT_TRANSFER_FROM_TYPEHASH, tokenPermissionsHash, msg.sender, permit.nonce, permit.deadline)
);
}
function hash(ISignatureTransfer.PermitBatchTransferFrom memory permit) internal view returns (bytes32) {
uint256 numPermitted = permit.permitted.length;
bytes32[] memory tokenPermissionHashes = new bytes32[](numPermitted);
for (uint256 i = 0; i < numPermitted; ++i) {
tokenPermissionHashes[i] = _hashTokenPermissions(permit.permitted[i]);
}
return keccak256(
abi.encode(
_PERMIT_BATCH_TRANSFER_FROM_TYPEHASH,
keccak256(abi.encodePacked(tokenPermissionHashes)),
msg.sender,
permit.nonce,
permit.deadline
)
);
}
function hashWithWitness(
ISignatureTransfer.PermitTransferFrom memory permit,
bytes32 witness,
string calldata witnessTypeString
) internal view returns (bytes32) {
bytes32 typeHash = keccak256(abi.encodePacked(_PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, witnessTypeString));
bytes32 tokenPermissionsHash = _hashTokenPermissions(permit.permitted);
return keccak256(abi.encode(typeHash, tokenPermissionsHash, msg.sender, permit.nonce, permit.deadline, witness));
}
function hashWithWitness(
ISignatureTransfer.PermitBatchTransferFrom memory permit,
bytes32 witness,
string calldata witnessTypeString
) internal view returns (bytes32) {
bytes32 typeHash =
keccak256(abi.encodePacked(_PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB, witnessTypeString));
uint256 numPermitted = permit.permitted.length;
bytes32[] memory tokenPermissionHashes = new bytes32[](numPermitted);
for (uint256 i = 0; i < numPermitted; ++i) {
tokenPermissionHashes[i] = _hashTokenPermissions(permit.permitted[i]);
}
return keccak256(
abi.encode(
typeHash,
keccak256(abi.encodePacked(tokenPermissionHashes)),
msg.sender,
permit.nonce,
permit.deadline,
witness
)
);
}
function _hashPermitDetails(IAllowanceTransfer.PermitDetails memory details) private pure returns (bytes32) {
return keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, details));
}
function _hashTokenPermissions(ISignatureTransfer.TokenPermissions memory permitted)
private
pure
returns (bytes32)
{
return keccak256(abi.encode(_TOKEN_PERMISSIONS_TYPEHASH, permitted));
}
}
SignatureVerification
SignatureVerification
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import {IERC1271} from "../interfaces/IERC1271.sol";
library SignatureVerification {
/// @notice Thrown when the passed in signature is not a valid length
error InvalidSignatureLength();
/// @notice Thrown when the recovered signer is equal to the zero address
error InvalidSignature();
/// @notice Thrown when the recovered signer does not equal the claimedSigner
error InvalidSigner();
/// @notice Thrown when the recovered contract signature is incorrect
error InvalidContractSignature();
bytes32 constant UPPER_BIT_MASK = (0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
function verify(bytes calldata signature, bytes32 hash, address claimedSigner) internal view {
bytes32 r;
bytes32 s;
uint8 v;
if (claimedSigner.code.length == 0) {
if (signature.length == 65) {
(r, s) = abi.decode(signature, (bytes32, bytes32));
v = uint8(signature[64]);
} else if (signature.length == 64) {
// EIP-2098
bytes32 vs;
(r, vs) = abi.decode(signature, (bytes32, bytes32));
s = vs & UPPER_BIT_MASK;
v = uint8(uint256(vs >> 255)) + 27;
} else {
revert InvalidSignatureLength();
}
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) revert InvalidSignature();
if (signer != claimedSigner) revert InvalidSigner();
} else {
bytes4 magicValue = IERC1271(claimedSigner).isValidSignature(hash, signature);
if (magicValue != IERC1271.isValidSignature.selector) revert InvalidContractSignature();
}
}
}
最後に
今回の記事では、Bunzzの新機能『DeCipher』を使用して、「Permit2」コントラクトを見てきました。
いかがだったでしょうか?
今後も特定のNFTやコントラクトをピックアップしてまとめて行きたいと思います。
普段はブログやQiitaでブロックチェーンやAIに関する記事を挙げているので、よければ見ていってください!
Discussion