Uniswapに沈む時価5億円以上の流動性について解説します。いわゆる「GOXした」というやつです。 超弩級のやらかし案件なので、そういった事例として興味深いと思います。
そして、資金を救える人も勝手に募集します!誰か我こそはという人は是非!もし救助できたら謝礼をもらえると思います。
概要
端的に申し上げますと、以下のERC20トークンの流動性がロックされてしまい救助不能に陥っております。
トークンの名前は「USD」です。大変ググラビリティの低い名前であり、普通の人がこのトークンを見つけることはないでしょう。 わたしは、botのトレードのために色々と調査していた際に偶然このトークンを発見しました。
最大の流動性は、USD-ETHペアのプールで供給されています。Uniswapの公式ページからこの流動性を見てみましょう。
一見して、異様な状況であることがお分かりいただけると思います。
- 名前からしてステーブルコインであると思われるのに異常に高い値段がついており全くペグ出来ていない。
- 24時間の取引高がゼロ、それどころか、最後にプールに対して操作が行われたのは541日前である
- 合計で、1939ETH(時価5億円以上)というかなりの流動性が供給されている
どうして流動性が回収されていないのか?
最初にこのトークンを発見した際に筆者は、詐欺トークンかなにかであろうと推測しました。
ERC20トークンを作成する際に、Transferに制限をかけることで、片方向のSwapしか出来ないようにして、資金の窃取を狙う手口があるからです。 くわしくは、以前に書いた以下の記事をご覧ください。
結論から言うとこれは当たらずとも遠からずでした。
ERC20トークンの実装にバグがあり、一切のTransferが出来ない状況になっていることが分かりました。(Approveすら動きません) つまり、流動性を回収しようとしても回収できないし、スワップも一切できません。 但し、詐欺の類ではありません。
実装のバグ
以下が実装のバグであると思われます。(間違っていたら教えてください)
このUSDトークンは、特殊なトークンで、SHAREという名前の別のERC20トークンのコントラクトとやり取りをします。 Transferの際もこのSHAREトークンのコントラクトのメソッドを呼び出そうとします。
ところが、この二つのコントラクトの間で、メソッドの宣言と定義が異なってしまっています。
USDトークンがTransferを行う際に、modiferからSHAREトークンのメソッドを呼ぼうとするのですが、 USDトークンで宣言されている型とSHAREトークンでの実装の定義が異なってしまっているために、 Transferが失敗します。
USDトークンでの宣言
142 interface ISeigniorageShares {
143 function setDividendPoints(address account, uint256 totalDividends) external returns (bool);
144 function mintShares(address account, uint256 amount) external returns (bool);
145 function lastDividendPoints(address who) external view returns (uint256);
146 function externalRawBalanceOf(address who) external view returns (uint256);
147 function externalTotalSupply() external view returns (uint256);
148 }
SHAREトークンの実装
468 function setDividendPoints(address who, uint256 amount) external onlyMinter {
469 require(who != address(0), "Invalid recipient address");
470 _shareBalances[who].lastDividendPoints = amount;
471 }
なぜ大きな流動性が残されているのか?
最初は動いていたからです。 途中でERC20トークンの実装を更新した際に、エンバグしました(やらかし1)。 エンバグというのは、デバッグの対義語で、バグを入れ込んでしまうことを言います。
ERC20トークンの仕様を更新することってできるの?
ERC20トークンの仕様、つまりスマートコントラクトの更新が出来るのかと疑問に感じる向きもあるかもしれません。 一度デプロイされたコントラクトを上書きすることはできません。 しかし、同じコントラクトアドレスを使いながら、実装をうまく変更する仕組みが考え出され広く利用されています。 これは、Proxyコントラクトと呼ばれるパターンを使って簡単に実現できます。
このUSDトークンも、 Proxyコントラクト としてデプロイされています。 Proxyコントラクトは、処理を別のアドレスにあるコントラクトにルーティングしています。 多段処理になっていて、実際の処理はルーティング先のImpelmentationコントラクトにコーディングされているわけです。 このルーティング先を変更すると、実装を変更できるようになっています。 ルーティング先を書き換えるメソッドを用意しておけば、コントラクト自体を上書きしなくても実装を変更することができます。
今現在は、Transferにバグがあるコントラクトへ処理がつながっているために、処理が失敗してしまいますが、最初は問題のないコントラクトへルーティングされていたわけです。 ある時点から動かなくなってしまったので、流動性が取り残されてしまっているのですね。
なぜバグを修正できないのか?
ここが一番の問題です。Proxyコントラクトは実装を更新するために使われます。 ではなぜ実装を更新してバグを直せないのでしょうか?
その理由は、ProxyコントラクトのOwnershipの操作を誤ったからです(やらかし2)。 エンバグ自体も問題ですが、こちらが大問題です。詳しく説明します。
Ownershipの構成
Proxyコントラクトのルーティング先を書き換えることが出来るのは、コントラクトのOwnerだけです。 このOwnershipについて確認してみましょう。
USDトークンのコントラクトとSHAREトークンのコントラクトのOwnershipは、以下のような特殊な構成になっています。 二つのコントラクトのオーナーは通常のアドレスではなく、また別のコントラクトになっています。 つまり、“0x79c9c…”というアドレスにある、複数のproxyを管理できるinterface用のコントラクトを用いています。 このコントラクトのことを私は、Proxy Master と呼んでいます。
ちなみにここが分からないのですが、このような構成は一般的なのでしょうか? なぜこのようなややこしいことをしたのか筆者にはよく分かりません。管理の手間を減らそうとしたのかもしれませんが、 障害点を挿入してしまう割に大したメリットはないように思われます。 まあ、この点は重要な点ではありません。このような構成でもきちんと動かすことは可能です。
さて、ここからが問題なのですが、なんとこのProxy MasterのOwnerは、Proxy Master自身になっています。 これは誤操作によるものです。
通常コントラクトのOwnerは新しいOwnerに変更できるようになっています。 本件では、何らかの理由により、エンバグ後に本来のOwnerが以下のトランザクションを発行して、コントラクトのOwnerをコントラクト自身に変更してしまいました。 これによって、全てのコントラクトのOwnerがいずれかのコントラクトになってしまったので、Proxyを更新することが出来なくなり、バグの修正が不可能になってしまいました。 本来のOwnerにも打つ手がなかったようで、その時点から合計1939ETHの流動性はロックされ現在に至ります。
簡単に言うと、Ownerがこの操作ミスした時点で、全ての流動性提供者のLPトークン、トークンの保持者の全トークンが一斉にGOXしたということになります。
なぜこのような操作ミスが起こったのか不明ですが、この直前にも同じOwnerにOwnershipをTransferするなどの意味のない操作をしているため、 相当に気が動転していて操作を誤ったのかもしれません。
その他
以上が簡単なバグの概要になります。あとはいくつか補足的な事項について説明をします。
エンバグ後の対応
エンバグ時のホルダーでスナップショットを取って、新しいトークンを配布したようです。 とはいえ、失った流動性は取り戻せないのでトークン自体の価値は大きく下がったと考えてよいでしょう。 その後何度かのアップデートなどを経て現在はプロジェクトはクローズしたようです。
Proxyコントラクトの危険性
Proxyコントラクトは、同じアドレスを使いながら、コントラクトの処理をアップデートしたり、バグ修正を行うことが出来るため大変便利なものです。
多くの有名なプロジェクトがこの仕組みを使っており、特にDeFiにはなくてはならないものと言えるでしょう。
但し、Proxyコントラクトはトラストフルで危険性を持つということは利用者として認識せねばならないでしょう。
多くのプロジェクトが、コードが監査済みであるということを強調します。 また実装はオープンになっていて、脆弱性がないことを実際にコードを読んで確認することもできます。 しかし、Proxyコントラクトのオーナーが、ある日突然実装のルーティング先をどう変更することも出来る状況では、 実装がオープンであるとか監査済みであるということは究極的には安全を担保しません。 Proxyコントラクトは、Ownerに対する究極のトラストの上に成り立っているのです。
ProxyコントラクトのOwnershipが放棄(renounce)されていたらそれはトラストレスと言えます。 Ownershipの放棄は通常、Ownerをアドレスzero-addressにトランスファーすることで実現できます。 しかし、そのようなプロジェクトの方が少ないのが現状かと思います。
この事例のようにOwnerが操作を誤った、あるいはOwnershipが乗っ取られたなどの状況で資金が危険に晒されるリスクは頭に置いておいてもいいかもしれませんね。 もっと規模の大きなトークンでこのような単純なミスによるGOX事例が発生してしまうこともゼロとは言えません。
まとめ
Ownerがとあるコントラクトの操作を誤って2000ETH近くがGOXした。誰か助けてあげてほしい。
- USDトークンProxyコントラクト
- USDトークンImplementationコントラクト
- SHAREトークンProxyコントラクト
- SHAREトークンImplementationコントラクト
- Proxyマスターコントラクト