1. はじめに
こんにちは、ウォレット開発運用部アプリケーション開発グループの山崎です。今回で5回目のテックブログ執筆となります。
今回はブロックチェーンの世界でたまに話題に上がるFake Depositという脆弱性と、それに対する取引所の対策手法について一例をあげて解説していきたいと思います。
これを書くモチベーションとして英語では以下のDEPOSafeというFake Depositを検知するツールの論文など、Fake Depositについて解説している素晴らしい文献が存在するのですが、日本語では私が知る限りいい記事を見つけられなかったので、それなら自分で書いてしまおう!と思ったことにあります。
https://arxiv.org/pdf/2006.06419.pdf
というわけでこれから解説していきます。
2. Fake Depositとは?
最初にFake Depositについて簡単に説明します。Fake Depositとは主にERC-20トークンに由来する脆弱性のことで、取引所への攻撃の場合、実際には入金されていないのに、取引所側でのトランザクション検証が不十分なことを利用して、「Fake Deposit」つまり「見せかけの残高」が存在するようにし、取引所から資金を抜き取るという攻撃手法が存在します。
2020年8月に7,772のERC-20トークンにこの脆弱性があることが大学機関の調査によって明らかにされ、当時ブロックチェーン界隈で少し話題になりました。
引用:
今回の記事ではERC-20トークンに由来するFake Depositの脆弱性をついた攻撃の具体的な手法と、取引所の対策例について説明していきます。
そしてこれを理解する上で、ERC-20のメソッドへの理解が必要なので、まずはこれについて説明します。
3. ERC-20のメソッド
ERC-20はファンジブルトークンを作る上での標準仕様を規定しているものです。つまりERC-20に則ってトークンを作成すれば比較的簡単にEthereum上のトークンを作成することができます。
ERC-20トークンを作る上で実装の必要があるメソッドは以下になります。
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
引用:
今回は transfer
メソッドの実装例を実際に見ていきながらFake Depositについて解説していきます。transfer
メソッドとは、 transfer
メソッドを呼び出したアカウントから受信者へトークンを移動させるメソッドで、そのオペレーションが成功したかをbooleanで返します。
引用:
4. Fake Depositの具体的手法
まずはSlowmistが公表した攻撃例のトランザクションを見てください。
https://etherscan.io/tx/0x9fbeeba6c7c20f81938d124af79d27ea8e8566b5e937578ac25fb6c68049f92e
引用:
このトランザクションでは ERC-20 Token Transfer Error (Unable to locate corresponding Transfer Event Logs)
とエラーが出ており、トークンの移動に失敗しているにもかかわらず、 Status
は Success
となっています。なぜこのような事象が起きているのでしょうか?
以下が DEPOSafe
の論文で An example of a vulnerable implementation of transfer
つまり transfer
メソッドに脆弱性がある実装例です。
function transfer(address _to, uint256 _value) returns (bool success) {
if (balanceOf[msg.sender] >= _value && balanceOf[_to] + _value > balanceOf[_to]) {
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;
} else {
return false;
}
}
引用:
https://arxiv.org/pdf/2006.06419.pdf
上の実装例では送信者( msg.sender
)が送金すべきトークンを十分に持っているかを条件分岐によって判定しています。もし、残高が不足していれば、7行目の else
以下に処理が入るため、 transfer
メソッドは false
を返し、実際の残高の移動は行われません。
しかし、 false
が返ったとしても例外が投げられていないため、トランザクションは止まらず、Status
は Success
となってしまいます。
これを防ぐためにEIPでは以下の記述のように、送信者の残高が不足していた際は例外を投げることが推奨されています。
The function SHOULD throw if the message caller's account balance does not have enough tokens to spend.
引用:
そのために、Solidity v0.8.0 以前はOpenZepplinのSafeMathのようにオーバーフロー時に例外を投げてくれる安全な数学関数を使うと良いとされていました。
ただしSolidity v0.8.0から、オーバーフローチェックがデフォルトで行われるようになったので、現在はSafeMathを使う必要はありません。
引用:
Solidity v0.8.0 Breaking Changes — Solidity 0.8.19 documentation
5. 取引所の対策例
これまで見てきた通り、単純なトランザクションの Status
の成否だけを見て、トランザクションを取り込んでしまうと、スマートコントラクトの実装に脆弱性があるトークンでは実際には残高が移動していないのに誤って取り込んでしまう危険性があります。
なので取引所では Status
による判断に加え、二次的な判定も加える必要があります。
例えば、Ethereum系の通貨は入金用アドレスに着金した時点では残高として認識せず、入金用アドレスから別のアドレスに残高を実際に送金できることが確認できた時点ではじめて残高として認識するという手法がその一つの例です。
6.最後に
Fake Depositという脆弱性と、それに対する取引所の対策手法の一例について解説してきました。このように取引所ではお客様の資産を安全に管理するために、様々な観点から脆弱性への対策を講じていることを垣間見ていただければ幸いです。
最後にコインチェックではエンジニアを積極募集中です。今話題のWeb3、ブロックチェーンの領域で勝負したいエンジニアの方などご興味ある方はお気軽にお申し込みください。
engineer-recruit.coincheck.com
またコインチェックに興味がある方、転職を検討している方に向けて社内の情報をゆるく発信していくCoincheck FMもやっておりますので、よろしければ聞いてみていただけると幸いです。