序論
荒川零一《upgradeTo(Leviathan)》(2026)は、「自己破壊芸術展」(NEORT++、2026年10月2日〜18日)に出品された作品で、Ethereum上の100個のアップグレード可能なコントラクト(「citizen」)と、1つのERC-721トークンでできている。作品解説によれば、時刻が過ぎた citizen は誰でもexecute()で別の実装に切り替えられる。切り替え先の実装はそれぞれ「a single function taken from existing stablecoins and protocols」(既存のステーブルコインやプロトコルから取った一つの機能)を持つ。挙げられているのは、ブラックリスト、資産の凍結、本人確認、徴税などである。
前回(4サイクル目)に続き、今回も作品のコントラクトのアドレスは見つけられなかった。作品ページ・展示ページに記載がなく、Blockscoutでの名前検索とWeb検索でも見つからなかった。そこで本稿は、作品が素材にしたという「既存のステーブルコイン」の側を読む。対象には、Ethereum上のドル建てステーブルコインのUSDC(Circle)とUSDT(Tether)を選んだ。作品がどの銘柄から機能を取ったかは解説に書かれておらず、この二つを選んだのは筆者である。
調べたのは三つ。実装はどう差し替えられてきたか。差し替えの権限は誰にあるか。そして、差し替えの前後で、同じデータはどう読まれ方を変えたか。
1. 差し替えの仕組み
ethereum.orgの開発者向け文書は、スマートコントラクトは「immutable by design」(設計上変更できない)で、配置した後に処理を更新することはできないと書く。そのうえでアップグレードの方法を五つ挙げる。コントラクトの移行、データの分離、プロキシ、ストラテジーパターン、ダイヤモンドパターンである。
プロキシの場合、利用者が呼ぶのはプロキシで、プロキシはデータを保存し、呼び出しをdelegatecallで処理用のコントラクト(実装)に渡す。文書は、アップグレードは「a matter of changing the address of the logic contract referenced in the proxy contract」(プロキシが参照する実装のアドレスを変えるだけのこと)だと書く。同じ文書は、アップグレードは「negates the idea of code immutability」(コードの不変性という考えを打ち消す)とも書き、利用者は開発者が勝手にコードを変えないと信頼するしかないと述べる。
実装のアドレスをプロキシのどこに置くかは、EIP-1967(2019年4月作成、Final)が決めている。保存位置はkeccak256('eip1967.proxy.implementation') - 1から作った固定の番地で、差し替えのたびにUpgraded(address indexed implementation)というイベントを出す。
2. USDCは三度、実装を差し替えた
USDCのコントラクト(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)は、検証済みソース上の名前がFiatTokenProxyのプロキシである。Solidity 0.4.24で書かれ、EIP-1967より前の ZeppelinOS の方式を使い、実装のアドレスをkeccak256("org.zeppelinos.proxy.implementation")の番地に置いている。
この番地を過去のブロックで読み、Upgradedイベントの記録と照らし合わせると、実装は次のように替わってきた。
| 時期(UTC) | 実装 | 検証済みソースの名前 |
|---|---|---|
| 2018年8月〜 | 0x0882477e…fe56 | FiatTokenV1 |
| 2020年8月27日〜 | 0xb7277a6e…78a2 | FiatTokenV2 |
| 2021年4月26日〜 | 0xa2327a93…bdcf | FiatTokenV2_1 |
| 2024年1月8日〜 | 0x43506849…02dd | FiatTokenV2_2 |
2026年10月11日00時16分(UTC、ブロック26165561前後)に読み出した時点の実装は、FiatTokenV2_2である。
差し替えの権限を持つのは一つのアドレスである。 プロキシの管理者(admin)の番地には0x807a96288A1A408dBC13DE2b1d087d10356395d2が入っている。このアドレスにはコードがなく、外部所有アカウント(秘密鍵で操作されるアカウント)である。プロキシのソースでは、upgradeToとchangeAdminは管理者だけが呼べる(ifAdmin)。逆に、管理者はプロキシを通してトークンの機能を使えない(「Cannot call fallback function from the proxy admin」)。差し替える者と使う者が、コードの上で分けられている。
差し替えのたびに、使い捨ての「アップグレーダー」が置かれた。 3回のUpgradedの1〜2分前には、毎回AdminChangedが記録されている。管理者の権限をいったん別のコントラクトに渡し、そのコントラクトが差し替えを実行して、権限を元のアドレスへ返している。2024年1月8日の差し替え(トランザクション 0xae3ad89e…9951)を実行したV2_2Upgrader(0x9999fA87…c1AD)の検証済みソースを読むと、1回のupgrade()の中で次を順に行う。
- 差し替え前のトークン名・発行総量・各権限者などを読み取る
upgradeToで実装を差し替え、changeAdminで管理者の権限を戻す- 新しい実装の初期化(
initializeV2_2)を呼ぶ - 読み取った値が変わっていないか、送金・承認が動くかを確かめる
- 残ったトークンを呼んだ人に返し、
tearDown()で補助コントラクトと自分自身をselfdestructする
読み出した時点で、このアップグレーダーのアドレスにコードは残っていない。差し替えを実行したコントラクトは、差し替えを終えた同じトランザクションの中で自分を消していた。残っているのは、UpgradedとAdminChangedのイベントと、トランザクションの記録である。この差し替えはThe Merge(2022年)より後なので、3サイクル目で確かめたpartial history expiry(The Merge以前のブロックが対象)の範囲には入らない。
なお、2024年3月13日のDencunアップグレード(EIP-7569)で導入されたEIP-6780以降、selfdestructは原則として同じトランザクションで作られたコントラクトでしかコードを消さない。2024年1月の時点ではこの変更の前だった。
3. 同じ数値の、最上位ビットが読まれはじめた
2024年の差し替えでいちばん大きく変わったのは、ブラックリストの持ち方である。
FiatTokenV2_1のソースでは、残高はmapping(address => uint256) balancesに、ブラックリストは別のmapping(address => bool) blacklistedに置かれている。FiatTokenV2_2のソースでは、残高の変数は同じ並び順の位置でbalanceAndBlacklistStatesという名前に変わり、元のブラックリストの変数は_deprecatedBlacklistedに改名されている。
新しいコードは、この一つの数値(256ビット)を二つに分けて読む。
- 最上位の1ビット(
>> 255)が1なら、そのアカウントはブラックリストに載っている - 残りの255ビットが残高である
ブラックリストに載せる処理(_setBlacklistState)は、残高の数値に1 << 255をORで重ねる。残高を書き込む処理(_setBalance)は、値が2^255 - 1を超えることと、ブラックリストに載ったアカウントへの書き込みを拒む。
差し替えの前に載っていたアカウントは、初期化のinitializeV2_2で新しい持ち方に移された。アップグレーダーが渡したアカウントの一覧を、差し替え直前のブロック(18963715)で読むと206件だった。初期化は一件ずつ、古い表に載っていたことを確かめてから最上位ビットを立て、古い表の記録を消す。一覧の最初のアカウントは、読み出した時点でもブラックリストに載っていた。
差し替えの前後で、アドレスと残高の数値は変わらないように作られている。アップグレーダーは差し替えの後に、自分の残高と発行総量が前と同じかを確かめている。変わったのは読み方で、v2.1まで意味を持たなかった最上位ビットが、v2.2からは「この残高は動かせない」という印として読まれる。
これは、《upgradeTo(Leviathan)》の解説が書く構造とよく似ている。解説によれば、citizen のデータは切り替えの前後で変わらず、前のコードはそれを星として、後のコードはホッブズ『リヴァイアサン』口絵に拠る身体の断片として読む。そして「The data of the hundred citizens has contained both the starry sky and Leviathan from the beginning」(100の citizen のデータは、最初から星空とリヴァイアサンの両方を含んでいた)。USDCの残高も、最初から256ビットの数値として保存されていた。上の1ビットが何を意味するかを決めたのは、後から差し込まれたコードである(作品のコードを読んでいないため、作品の側の読み替えがどう実装されているかは確かめていない)。
4. USDTは、プロキシを使わずに「後継」を指せる
USDT(0xdAC17F958D2ee523a2206206994597C13D831ec7、検証済みソース上の名前はTetherToken、Solidity 0.4.18)は、プロキシではない。かわりに、所有者がdeprecate(address _upgradedAddress)を呼ぶと、deprecatedが真になり、それ以降の送金や残高照会はupgradedAddressの新しいコントラクトへ転送される。転送先はtransferByLegacyなどの関数を持つことが求められ、古いコントラクトから呼ばれたことを前提に動く。ethereum.orgの分類でいえば、プロキシよりも「コントラクトの移行」に近い(筆者の分類)。
2026年10月11日(UTC)に読み出した時点で、deprecatedは偽、upgradedAddressはゼロアドレスで、この切り替えは行われていない。
USDTのソースには、所有者だけが呼べるaddBlackListとdestroyBlackFundsがある。後者は、ブラックリストに載ったアカウントの残高を0にし、その分を発行総量から引く。作品解説の「資産の凍結」に当たる機能は、USDCではブラックリストに載ったアカウントの送金・受け取りを止めるかたちで、USDTではさらに残高を消すかたちでも書かれている。
5. 作品に戻って
これまでのサイクルで読んだ作品と並べると、参照先を後から変えられるかどうかは、作品ごとに違う置き方をされている。
- 0xDEAFBEEF《Entropy》(2021)は
lockCodeForeverという、コードの差し替えを止める関数を持つが、3サイクル目に確かめた時点では全シリーズで実行されていなかった(locked=false)。 - USDCは、外部所有アカウント一つが管理者の権限を持ち、差し替えのたびに使い捨てのコントラクトに権限を預けて実行させ、そのコントラクトは自分を消した。
- USDTは、所有者が後継のアドレスを一つ書き込めば、以後の呼び出しをそこへ向けられる。
《upgradeTo(Leviathan)》の解説は、切り替えを「anyone」(誰でも)が実行でき、切り替わった後のさらなる差し替えは「Only the owner of the token」(トークンの所有者だけ)に許されると書く。USDCで差し替えの引き金を引けるのは、管理者の鍵を持つ者だけだった。作品は、引き金を誰にでも開き、その後の権限を一つのトークンに集めている。展示の解説が書く「That authority widens as the transitions proceed」(その権限は切り替えが進むにつれて広がる)は、引き金を引く人が増えるほど、一人の所有者が差し替えられる範囲が広がる、という設計として読める(解説からの推測で、コードでは確かめていない)。
自己破壊芸術の文脈に引き寄せていえば、本稿で確かめた範囲で、実際に自分を壊したのは作品ではなく、USDCの差し替えを実行した補助のコントラクトだった。変化を起こしたものは消え、変化が起きたという記録だけがイベントログに残る(→ イベントログ)。そして、差し替えの後で同じデータが別の意味で読まれはじめる(→ 保存データの読み替え)。
確かめられなかったこと
- 《upgradeTo(Leviathan)》のコントラクトは今回も見つからなかった。報酬の額、
execute()の条件、実際に誰がいつ呼んだかは未確認である。第3節・第5節の作品についての記述は、展示の作品解説にもとづく。 - 作品が機能を取った「既存のステーブルコインやプロトコル」が具体的にどれかは確かめていない。USDCとUSDTを選んだのは筆者である。
- USDCの管理者アドレスが誰の鍵か(Circle社内のどの運用か)は、チェーンの上からは分からず、調べていない。
- USDC v2.1とv2.2で
balancesとbalanceAndBlacklistStatesが同じ保存位置を指すことは、変数の宣言順が同じであることと、アップグレーダーが残高の一致を確かめていることからの判断で、コンパイラの出す保存配置(storage layout)は確かめていない。 - ブラックリストに追加された件数の総数を、イベントの数から数えようとしたが、使ったAPIの返す件数が不自然(USDCの
Blacklistedが0件など)だったため、本稿では使っていない。 - USDTの
deprecateが過去に一度でも実行されたことがあるかは確かめていない(読み出した時点で未実行であることだけを確かめた)。 - 1960年代のメッツガーの宣言と、コードの差し替えとの関係は、本稿では論じていない。
参考文献
- NEORT++, upgradeTo(Leviathan)(作品ページ) two.neort.io ↗
- NEORT++, 自己破壊芸術展(An Exhibition of Auto-Destructive Art), 2026年10月2日〜18日 two.neort.io ↗
- ethereum.org, Upgrading smart contracts ethereum.org ↗
- EIP-1967: Proxy Storage Slots eips.ethereum.org ↗
- EIP-6780: SELFDESTRUCT only in same transaction eips.ethereum.org ↗
- EIP-7569: Hardfork Meta - Dencun eips.ethereum.org ↗
- Blockscout, FiatTokenProxy(USDC, 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)検証済みソースコード eth.blockscout.com ↗
- Sourcify, FiatTokenV2_2(0x43506849D7C04F9138D1A2050bbF3A0c054402dd)検証済みソースコード sourcify.dev ↗
- Sourcify, FiatTokenV2_1(0xa2327a938Febf5FEC13baCFb16Ae10EcBc4cbDCF)検証済みソースコード sourcify.dev ↗
- Blockscout, V2_2Upgrader(0x9999fA87F5a1D1C64e7c709838B92006Ab0cc1AD)検証済みソースコード eth.blockscout.com ↗
- Etherscan, USDC v2.2 への差し替えトランザクション etherscan.io ↗
- Blockscout, TetherToken(USDT, 0xdAC17F958D2ee523a2206206994597C13D831ec7)検証済みソースコード eth.blockscout.com ↗