JPYCv2のブロックリスト機能の紹介
こんにちは。JPYC研究開発チーム所属のretocroomanです。
今回は3月にアップデートしたJPYCv2の新機能の一つ、ブロックリスト機能を紹介します。AML(マネー・ローンダリング防止対策)が求められるステーブルコインではよく実装されている機能でこちらのPANewsの記事によるとUSDT、USDC、PAX等がブロックリストのような機能を持っているとありますが、意外と存在自体初めて聞いた方もいらっしゃるのではないでしょうか。
JPYCv2の開発に自分が関わったこともあり、JPYCv2に関してはかなり込み入った話まで解説できると思います。ただ、JPYCv2はCentreの規格に従っているUSDCに倣って様々な機能が実装されていることから、全てを解説するのはかなり大変です…そこで、まずはブロックリスト機能から順に紹介していこうと思います。アップグレード機能やメタトランザクション機能については今後記事にしていく予定です!
前提知識
Solidity(前提知識集)
Upgradeable(前提知識集)
AccessControl.solのコード解説
BlockListable.solの目的と概要
不正にトークンを取得したアドレスの資産を凍結させたり、詐欺に使われているアドレスへの送信を制限したりするのにブロックリスト機能は使われます。
USDC、USDT等のステーブルコインの機能として一般的であるブラックリスト機能とほぼ同じ機能ですが、ブラックリストという言葉が差別用語にあたる可能性を配慮し、JPYCv2ではブロックリストと命名しています。
ブロックリストを実装するに当たって、ブロックリストを管理するブロックリスターが必要ですが、これはBlocklistableコントラクトに実装してもいいですし、複数の権限を管理できるライブラリのAccessControlコントラクトでブロックリスターの権限を設定してもいいです。JPYCv2は前者を採用し、ブロックリスターはJPYCv2のownerが管理することにしています。
具体的にはブロックリストをトークンの残高のようにマッピング型でブロックリスト入りかどうかをストレージに保存し、トークンを送受信する際はそのストレージを読み出して確認しています。実はこれ以外にもガス代がかかるストレージの読み出しを避けるやり方も提案されており、後半ではそちらのコードもご紹介いたします。
Blocklistable.solのコード解説
JPYCv2の全体像
解説するコードのリンク
Blocklistable.sol(JPYCv2)
継承しているコントラクトのコードのリンク
Ownable.sol(JPYCv2)
Blocklistableが継承しているコントラクトの解説
contract Blocklistable is Ownable {
Blocklistableコントラクトはコントラクトのオーナーを管理するOwnableコントラクトを継承しています。JPYCv2のOwnableコントラクトはOpenZeppelinのOwanbleとほぼ同じコードですが少しだけ変更しています。変更点はバージョンが0.8.11であること、オーナー権を放棄する関数を削っていること、OpenZeppelinのContextライブラリを継承していないことです。
このOwnableコントラクトで定義されているowner
がブラックリスターの変更をすることができます。この他にもコントラクトの送金機能を停止できるpauser
や誤って送られてきたトークンを救出できるrescuer
などもowner
が変更することができます。
Blocklistableコントラクトは最終的にはトークンの基本ロジックが書かれているFiatTokenV1コントラクトへ継承されています。FiatTokenV1コントラクトではapprove
やtransfer
の関数が用意されており、そこでBlocklistableの修飾子が使われています。
Blocklistableの状態変数と修飾子の解説
// FiatTokenV1のinitialize関数で初期化される。
address public blocklister;
// 初期値は0でブロックリスト入りすると1になる
// ガス代節約のためboolではなくuint256を使用
mapping(address => uint256) internal blocklisted;
// ブロックリスト入りした時のイベント
event Blocklisted(address indexed _account);
// ブロックリスト解除した時のイベント
event UnBlocklisted(address indexed _account);
// ブロックリスターが変更された時のイベント
event BlocklisterChanged(address indexed newBlocklister);
// msg.senderがブロックリスターの権限を持っているかどうか確かめる修飾子
modifier onlyBlocklister() {
require(
msg.sender == blocklister,
"Blocklistable: caller is not the blocklister"
);
_;
}
// 引数のアドレスがブロックリストかどうか確かめる修飾子
modifier notBlocklisted(address _account) {
require(
// ブロックリスト入りしてなければ0
blocklisted[_account] == 0,
"Blocklistable: account is blocklisted"
);
_;
}
onlyBlocklister修飾子はBlocklistableコントラクトで、notBlocklisted修飾子は継承先のFiatTokenV1コントラクトで使用されています。FiatTokenV1コントラクトでnotBlocklisted修飾子が使われている箇所をまとめたのが下の表になります。
関数名 | 説明 | 確認される変数 | 意味 | 行数 |
---|---|---|---|---|
mint | トークン発行 | msg.sender | 送信者かつ発行者 | 156 |
mint | トークン発行 | to | 発行先 | 157 |
approve | トークン移動の許可 | mag.sender | 送信者かつ許可元 | 255 |
approve | トークン移動の許可 | spender | 許可先 | 256 |
transferFrom | トークンの移動 | msg.sender | 送信者 | 295 |
transferFrom | トークンの移動 | from | 移動元 | 296 |
transfreFrom | トークンの移動 | to | 移動先 | 297 |
transfer | 送信者によるトークンの移動 | msg.sender | 送信者かつ移動元 | 319 |
transfer | 送信者によるトークンの送信 | to | 移動先 | 320 |
burn | トークン焼却 | msg.sender | 送信者かつ焼却者 | 395 |
increaseAllowance | 移動を許可したトークンの増量 | mag.sender | 送信者かつ許可元 | 425 |
increaseAllowance | 移動を許可したトークンの増量 | spender | 許可先 | 426 |
decreaseAllowance | 移動を許可したトークンの減量 | mag.sender | 送信者かつ許可元 | 442 |
decreaseAllowance | 移動を許可したトークンの減量 | spender | 許可先 | 443 |
transferWithAuthorization | トークン移動のメタトランザクション | from | 移動元 | 505 |
transferWithAuthorization | トークン移動のメタトランザクション | to | 移動先 | 505 |
receiveWithAuthorization | トークン移動のメタトランザクション* | from | 移動元 | 543 |
receiveWithAuthorization | トークン移動のメタトランザクション* | to | 移動先 | 543 |
permit | トークン移動の許可のメタトランザクション | owner | 許可元 | 594 |
permit | トークン移動の許可のメタトランザクション | spender | 許可先 | 594 |
*トークンの移動先であるto
がメタトランザクションのリレイヤーであるmsg.sender
と同じになるよう制限しているもの
ちなみにブロックリストの管理にbool型よりuint256型の方がガス代が安くなるのは、ストレージに値はuint256(bytes32)型の状態で保存されるからです。bool型を使うとuint256型からbool型へ型変換する必要があるのでガス代が少しだけ高くなります。とはいってもガス代が変わらないケースもあり、このケースでは安くなったためuint256型を採用しました。
ところでコードの最後にuint256型の配列が宣言されていることに気付きましたか?
uint256[50] private __gap;
これです。これはupgradeableなコントラクトに特徴的な部分でストレージの空き容量を用意しています。upgradeableなコントラクトでアップグレードできるものはコントラクトのコードだけでストレージは場所も値も変更できません。なので空の配列を宣言しストレージの場所を確保することで、Blocklistableの状態変数を正しいストレージ場所に追加できるようになっているのです。もし、Blocklistableコントラクトに継承先が無ければ空き容量を確保しなくても問題ないのですが、FiatTokenV1コントラクトに継承されているので、そのまま追加しようとすると継承先のFiatTokenV1が使うストレージの場所が追加した分ずれてしまうのです。
isBlocklisted関数(78行目)の解説
// 引数のアドレスがブロックリスト入りしているかどうかbool型で返す
function isBlocklisted(address _account) external view returns (bool) {
// ブロックリスト入りは1で表している
return blocklisted[_account] == 1;
}
特に意味はありませんがブロックリスト入りを1で表しています。
blocklist関数(86行目)とunblBlocklist関数(95行目)の解説
// 引数のアドレスをブロックリストに入れる
// ブロックリスターのみ呼べる
function blocklist(address _account) external onlyBlocklister {
blocklisted[_account] = 1;
emit Blocklisted(_account);
}
// 引数のアドレスをブロックリスト解除する
// ブロックリスターのみ呼べる
function unBlocklist(address _account) external onlyBlocklister {
blocklisted[_account] = 0;
emit UnBlocklisted(_account);
}
unBlocklist関数で0を代入するよりdeleteを使用した方がガス代が安くなるという話もありますが、コンパイルする時にoptimizerでコードを最適化した場合はガス代は変わらなかったため、0を代入しています。
updateBlocklister関数(100行目)の解説
// ブロックリスターを引数のアドレスのアカウントに変更する
// ownerのみ呼べる
function updateBlocklister(address _newBlocklister) external onlyOwner {
// ブロックリスターに0は指定できない
require(
_newBlocklister != address(0),
"Blocklistable: new blocklister is the zero address"
);
// 新しいアドレスに変更
blocklister = _newBlocklister;
emit BlocklisterChanged(blocklister);
}
owner
のみがblocklister
を変更できるようになっていることが確認できます。
ガス代を節約できる新しいBlocklistable.solの提案
Duneのクエリ結果を見ての通り、2021年の12月中に3,600,000ドル、ユーザーがブロックリストのチェックのためだけにガス代を払っています。
これに対して提案された解決策が以下の二つの関数です。みていきましょう。
// 引数のアドレスのアカウントを凍結する
function freezeBalance(address _account) external onlyBlacklister {
// 凍結する量
uint256 amountFrozen = balance[_account];
// 総トークン発行量から凍結する量を引く
totalSupply_ = totalSupply_.sub(amountFrozen);
// アカウントの凍結残高に凍結する量を足す
frozenBalance[_account] = frozenBalances[_account].add(amountFrozen);
// アカウントの残高は0にする
balance[_account] = 0;
emit BalanceFrozen(_account, amountFrozen);
emit Transfer(_account, address(0), amountFrozen);
}
凍結対象のアカウントの残高を凍結残高に記録していますね。その際、総トークン発行量からも凍結する量を引いています。
// 引数のアドレスのアカウントを凍結解除する
function unfreezeBalance(address _account) external onlyBlacklister {
// 凍結解除する量
uint256 amountUnfrozen = frozenBalnces[_account];
// 総トークン発行量に凍結解除する量を足す
totalSupply_ = totalSupply_.add(amountUnfrozen);
// アカウントの凍結残高を0にする
frozenBalance[_account] = 0;
// アカウントの残高に凍結解除する量を足す
balance[_account] = frozenBalances[_account].add(amountUnfrozen);
emit BalanceUnfrozen(_account, amountUnfrozen);
emit Transfer(address(0), _account, amountUnfrozen);
}
凍結解除の際は記録しておいた凍結残高を使って残高を復活させています。
この方法なら、トークンの移動の際にブラックリストのチェックをしなくていいですね。ブラックリスト入りのアドレスに送られたトークンはブラックリスターによって残高が0にさせられるので、ブラックリスト入りしたアドレスからトークンを移動させることはできません。また、ブラックリスト入りのアドレスにトークンを送信するのは損でしかないので問題ないでしょう。そして、間違ってアカウントが凍結された場合でも凍結解除されると自動でトークンが返ってくるようになっています。
一見するとガス代削減の完璧な解決策のように見えますが、この提案にも二つの懸念点があります。
一つ目は、ブラックリスト入りのアドレスにトークンを送信できてしまうことです。これはユーザーが詐欺に引っ掛かり、詐欺アドレスにトークンを送信して資産をロストしてしまう恐れがあるということです。ブラックリストのチェックがあれば防げたリスクで新しい対応が迫られます。一つの対応策としてウォレット等のフロントエンドで詐欺アドレスについて警告するという方法が考えられます。
二つ目は、ブラックリスターがブラックリスト入りのアドレスへの送信を監視し続け無ければいけないということです。送信されたトークンをすぐに凍結しなければ意味がないので、トランザクションの監視コストがブラックリスターにかかり続けます。
またこの他にも、ブラックリストはかなり中央集権的な仕組みですので、USDCの発行体のCentreはガバナンスや法律の観点からの議論も必要と考えているそうです。JPYC社としても今後の規制の動向を踏まえアップグレードで対応するとしてJPYCv2ではこの方法は見送りました。
まとめ
JPYCv2のブロックリスト機能をみてきました。コード自体はとてもシンプルなので理解するのが簡単だったかもしれません。実際にブロックリスト機能を運用する際のルール決めや法規制への対応の方が難しいでしょう。
ブロックリスト機能はその中央集権性とコストから実装されないことも多いですが、銀行のような存在を目指すステーブルコインの発行業社はブロックリストに対応していると思います。実用例として、coinpostの記事には、USDCを発行する米国企業Centreが、法執行機関からの要請に応じてハッキングしたとされるアドレスの資産を凍結したことを報じられています。このような機能がハッキングやマネーロンダリングの抑止につながっているのは事実でしょう。
JPYCv2も、これから発行数を伸ばし社会的責任が増してくなかでブロックリストへの対応が必要不可欠だと認識しています。しかし、それはなるべくユーザーに負担がかからないかたちで実現していくべきなのも言うまでもありません。なるべく、ユーザーに負担が掛からないハッキングやマネーロンダリングの抑止策を模索していく所存です。
ここまでお読みいただきありがとうございます。JPYCv2の新機能について、今後も紹介していくのでお楽しみください!
日本初のブロックチェーン技術(ERC20)を活用した日本円ステーブルコインJPYCはこちらから購入できます!
JPYC社はブロックチェーンエンジニアを募集中です!こちらからご応募お願いします!(タイミングにより募集を行なっていない場合があります)
また、ラボ形式でブロックチェーンに関する講義をしているJPYC開発コミュニティにも是非ご参加ください!
Discussion
興味深い記事をありがとうございます!タグですが、おそらく
ethrerum
ではなくethereum
を意図しているとおもいますので、お手隙で修正いただけると嬉しいです!