はじめに
DeFierの皆さん、こんにちは。最近エアドロップがなくて懐が寂しいビビドット (@vividot) | Twitterです。
1週間前、RUNE というトークンのホルダーに対して攻撃がありました。 今回は攻撃の発生から注意喚起までが早かったので実際に被害にあったのは3名ほどのようですが、約36ETHが攻撃者によって奪われています。 DeFiでよく聞く攻撃の被害額と比べると少額ですが、個人単位で見ると10ETHでも痛い被害だと思います。
そもそも RUNE というのはTHORChainのネイティブトークンでCoingeckoによると時価総額は18億ドルで58番目に大きいトークンです。(7月31日現在)。 また、そのうち約1億ドルがERC-20としてEthereum上で流通しており、ホルダーは1万を超えます。
本記事では、この攻撃について何が起きたのか、なぜ起きたのか、ユーザはどうするべきかを解説します。
※話を簡単にするため、簡略化した説明を含みます
何が起きたのか
まず、UNI HODLというトークンが攻撃者によって約7000人にエアドロップされました。 ホルダーをリストアップして確かめた訳ではありませんが、エアドロップの対象者はRUNEのホルダーだと考えられます。
そして、エアドロップの直前に、攻撃者はUniswapに対して流動性の供給を行っており、100UNIHで0.08ETHにスワップすることができるように見えます(たぶん出来ない)。
100 UNIH受け取ったユーザは「お?エアドロか、0.08ETHラッキー」と売ろうとするでしょう。僕ならします。
しかし、それは罠です。RUNE というトークンに脆弱性があり、UniswapでUNIHをApproveするTXを送信したユーザは自身の RUNE を全て攻撃者に取られてしまいました…。
なぜ起きたのか
RUNE にあった脆弱性は「Tx.Origin Authentication」として知られています。平たくいうとスマートコントラクトでtx.originという変数をユーザの認証に使うとヤバイヨということです。
tx.origin とは?
tx.originとは、スマートコントラクトで利用できる変数の1つでトランザクションの送信者のアドレスを意味します。 また、類似する変数としてmsg.senderというものがあり、こちらはインターナルトランザクションも含めたトランザクションの送信者のアドレスを意味します。
インターナルトランザクションというのは、コントラクトが起点となって他のコントラクトを利用する際に内部的に発行される物と思ってもらって構いません。 これを示したのが次の図です。
ユーザはスマートコントラクトAに対してTXを送信していますが、スマートコントラクトAは内部でスマートコントラクトBを呼び出しています。 この部分、2つ目の矢印がインターナルトランザクションを意味します。 このとき、スマートコントラクトBにおいてmsg.senderはスマートコントラクトAのアドレスになりますが、tx.originはユーザのアドレスになります。
どのように悪用されるか
スマートコントラクトにおいて、ユーザのアドレスをキーとしてデータを管理することが多々あります。 例えば、ERC-20トークンではアドレスとアドレスに対応する残高を次のようなイメージで管理しています。
アドレス | 残高 |
---|---|
Aさんのアドレス | 1000 Token |
Bさんのアドレス | 1500 Token |
Cさんのアドレス | 500 Token |
そして、BさんがCさんに1000 Tokenを送金したいとき、transfer関数を呼ぶことで、スマートコントラクトでは次のような操作が一般的に行われています。
1. msg.sender(つまりBさんのアドレス)の残高から1000引く 2. 送金先(つまりCさんのアドレス)の残高に1000足す
では、このtransfer関数をコントラクトから呼んでみるとどうなるでしょうか? 例えば、UniswapなどのDEXでTokenをETHにSwapするときを想像してください。 DEXがtransfer関数を呼ぶとmsg.senderがDEXのアドレスになってしまうためBさんの残高を変更することはできないことが分かるかと思います(Uniswap自身の残高が動くことになる)。
だからこそ事前にapproveする必要性があって、DEXがユーザの残高を変更したい場合にはtransferFrom関数を用いて次のような操作が一般的に行われています。
1. 送金元がmsg.sender(つまりDEX)に対して残高の変更を許可(approve)しているか 2. 送金元(つまりBさんのアドレス)の残高から1000引く 3. 送金先(つまりCさんのアドレス)の残高に1000足す
RUNEの場合
そして今回被害にあったRUNEですが、標準のERC-20に加えてtransferToという関数が開発者によって追加されていました。 このtransferTo関数を用いて、BさんがCさんに1000 RUNE送金したい場合、次のような操作が行われます。
1. tx.origin(つまりBさんのアドレス)の残高から1000引く 2. 送金先(つまりCさんのアドレス)の残高に1000足す
何も問題ありませんね。では、コントラクトからtransferTo関数を呼んでみるとどうなるでしょうか?
なんとapproveなしにBさんの残高をコントラクトが動かすことができてしまいます。これが脆弱性です。
しかし、この脆弱性を突いた攻撃は攻撃者のコントラクトに対してTXが送信されないと成功しません。 そこで、今回の攻撃者はUNI HODLというトークンをエアドロップし、あたかもUniswapで0.08ETHというそこそこの額で売れると見せかけてユーザがapproveするように仕向けた訳です。
ユーザはどうするべきか
tx.originを認証に使うと危険、というのは、ほぼ全ての開発者が知っていると思います。 また意図していない場合であれば監査の際に指摘され修正されると思います。
しかし、RUNEのように意図して実装(後述)しているコントラクトが存在する可能性が否めません。 そうなってくると自身の資産に関与しているコントラクトの全てに脆弱性がないかをチェックするか、安易に見知らぬコントラクトに対してTXを送信するのを控えるかしかありません。
もちろん前者はとても現実的ではないので、後者の「安易に見知らぬコントラクトに対してTXを送信するのを控える」を徹底するべきです。
具体的に「見知らぬコントラクト」とは
見知らぬコントラクトとは、デプロイされたばかりで開発者が正体不明なコントラクトです。
tx.originを突いた攻撃は、無作為に仕込むよりエアドロップなどで対象者を絞ったほうが攻撃者側からすると費用対効果が高いです。 そのため、今回のようにアドレスに対して直接配布されたトークンのコントラクトやサイトでエアドロップをClaimするためのコントラクトは注意が必要です。 特にEtherscanでソースコードがVerify&Publishされていないコントラクトは第三者がどのような動作をするのか確認できません。
また、コントラクトに送信している他の人のTXを見て、何も起きてないから安全と考えるのは早計です。 というのも、もし自分がこの攻撃をするのであれば、5000万円以上の残高がなければRUNEを盗まないといった条件を入れて、より高額な窃取を狙うからです。
その場合でも、TXをしっかりチェックすれば残高を確認してるかどうか分かる(Etherscanではできません)ので、誰かがチェックして安全が確認できてからTXを送信するようにしましょう。
「見知ったコントラクト」なら安全か
いいえ、100%安全ではありません。この脆弱性に対して100%安全なのは(たぶん)コントラクトウォレットを使っている場合のみです。
というのも、tx.originは非常に厄介で攻撃者のコントラクトへの呼び出しがTXに含まれているだけでアウトです。 イメージしやすいシチュエーションとしては、Uniswapや1Inchなどの自動ルーティングで攻撃者のコントラクトがパスにある場合です。
なので、100%安全を求めるのであれば、そういう所まで気を使わなければいけないのですが、そんな事してたら疲れ果ててしまうので、まずは安易に見知らぬコントラクトにTXをすぐ出さないことから始めてみましょう。
最後に
RUNEに脆弱性が存在していることは開発者も認知していました。 というのも、そもそも意図した目的でapproveを不要にしているからです。 コントラクトのソースコードにもしっかりとコメントで書いてあります。
すごいtx.origin attackだ!2021年にもなって既知の脆弱性が…と思ってRUNEのコントラクト見てきたら「フィッシングに注意してください」って書いてがっつり実装されてるけど、そんな無茶なwhttps://t.co/O0s5rZUuNM pic.twitter.com/ya72qLzSeh
— ビビドット (@vividot) 2021年7月23日
しかし、コントラクトのソースコードもドキュメントも確認しているユーザはごく少数であり、RUNEのホルダーでこの脆弱性を把握していた人が果たして何人いたのかは疑問です。
以上、RUNE事件に学ぶ安易にTXを送信することの危険性でした。