Proxy contractの読み方:ERC-1967 slot・implementation・admin・upgradeを確認する
ERC-1967のimplementation・admin・beacon slotをraw RPCで読み、Transparent・UUPS・Beacon、initializer、storage collision、upgrade履歴までlocal再現で検証します。

Proxy contractを読む出発点は、proxy addressがstateと外部callの入口、implementation addressが実行codeの置き場だと分けることです。Explorerにverified sourceや「proxy」と表示されても、現在の実行先、upgrade authority、過去の変更、storage layoutまで同時に安全だとは分かりません。
この記事では、ERC-1967のimplementation、admin、beaconという3つのstorage slotを同じblockで読みます。その後にbytecode、source、ABI、call behavior、authority、events、storage layoutを対応させます。調査はread-onlyとローカル再現だけで、公開RPC、実在wallet、署名、upgrade transaction、資産は使いません。
proxy addressとimplementation addressは役割が違う
通常のdelegatecall型proxyでは、利用者はproxy addressを呼びます。proxyはimplementationのcodeを実行しますが、address(this)、balance、storageはproxy側です。
| address | 主な役割 | 単独では分からないこと |
|---|---|---|
| proxy | user-facing address、state、balance、event emitter、delegatecall入口 | 現在・過去の実行code、upgrade authorityの全体 |
| implementation | function code、ABI候補、storage layoutの宣言 | proxyに保存された値、現在そのproxyが本当に参照するか |
| admin / owner | upgradeを開始できるauthority候補 | multisig、timelock、governance、owner履歴の健全性 |
| beacon | 複数proxyが共有するimplementation参照先 | beacon ownerと、各proxyに残る個別state |
implementationへ直接Readすると、そのimplementation自身のstorageを読みます。proxy経由の結果とは別です。Writeを送る前の基本確認は直コンでverified contract・ABI・シミュレーションを確認する順番、calldataとlogsの読み方はABI・function selector・event logsの手検算で確認できます。
ERC-1967の3 slotをraw RPCで読む
ERC-1967は、implementation、beacon、adminをimplementation側の通常storageと衝突しにくい固定位置へ置きます。各値は識別文字列のKeccak-256から1を引いた値です。
| slot | exact position | 32-byte wordの読み方 |
|---|---|---|
| implementation | 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc |
下位20 bytesがimplementation候補 |
| admin | 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 |
下位20 bytesがadmin候補 |
| beacon | 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50 |
下位20 bytesがbeacon候補 |
raw JSON-RPCでは、address、slot、block tagをeth_getStorageAtへ渡します。latestのまま証拠へ残すのではなく、先にblock numberを固定します。
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getStorageAt",
"params": [
"0xProxyAddress",
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"0xFixedBlockNumber"
]
}
viem 2.56.0では同じRPCを次のように3回読み、32-byte wordの下位20 bytesをchecksum addressへ変換できます。
import { getAddress, toHex } from 'viem'
const slots = {
implementation: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc',
admin: '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103',
beacon: '0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50',
} as const
const blockNumber = await publicClient.getBlockNumber()
const entries = await Promise.all(Object.entries(slots).map(async ([name, slot]) => {
const word = await publicClient.getStorageAt({ address: proxy, slot, blockNumber })
const normalized = word ?? toHex(0, { size: 32 })
return [name, { slot, word: normalized, address: getAddress(`0x${normalized.slice(-40)}`) }]
}))
console.log({ blockNumber, slots: Object.fromEntries(entries) })
zero wordは「その方式ではない」手掛かりですが、直ちにupgrade不能という意味にはなりません。custom proxyは別slotを使えます。nonzero addressも、getCode、runtime bytecode、source、ABI、call behaviorまで確かめて初めて役割の候補になります。
Transparent・UUPS・Beaconはcall pathとauthorityが違う
3方式ともdelegatecallを使えますが、「implementationをどこから得るか」と「upgrade functionを誰が呼ぶか」が違います。
| 方式 | proxyで主に見えるslot | normal call | upgrade entrypoint / authority |
|---|---|---|---|
| Transparent | implementation + admin | admin以外をimplementationへdelegatecall | ProxyAdminがproxyの専用dispatchを呼ぶ |
| UUPS | implementation。adminは通常zero | implementationへdelegatecall | implementation内のupgradeToAndCallと_authorizeUpgrade |
| Beacon | beacon。implementationは通常zero | beaconのimplementation()を読みdelegatecall |
beacon ownerがbeaconをupgradeし、接続proxy全体へ反映 |
Transparent Proxyはadminのcallだけ分岐する
OpenZeppelin Contracts 5.6.1のTransparentUpgradeableProxyは、構築時に専用ProxyAdminを作ります。normal callerのcallはimplementationへ進みます。一方、実adminであるProxyAdminからのcallはupgrade dispatchだけを受け付け、implementationの通常functionへfallbackしません。
ローカル再現ではimplementation slotがV1、admin slotがcodeを持つProxyAdmin、beacon slotがzeroになりました。ProxyAdmin callerのvalue()はProxyDeniedAdminAccessで失敗し、別callerのvalue()は41を返しました。
OpenZeppelin 5.xでは実admin addressをproxyのimmutableにも保持します。untrusted implementationがERC-1967 admin slotを書き換えると、raw slotと実際のadminが不一致になり得ます。したがってadmin slotは重要な手掛かりですが、implementationを信用できない状況で唯一のauthority証拠にはしません。
UUPSはimplementation側にupgrade codeがある
UUPSではproxyは基本的なERC1967Proxyで、upgrade functionはimplementationにあります。proxy経由でupgradeToAndCallを呼ぶとdelegatecallされ、implementation側のonlyProxy context checkとapplication固有の_authorizeUpgradeを通ってimplementation slotを書き換えます。
admin slotがzeroでもupgrade不能とは限りません。owner、role、multisig、timelockなど、_authorizeUpgradeが読むstateと変更履歴を実装sourceから追います。新implementationのproxiableUUID()がERC-1967 implementation slotを返す互換性checkも必要です。
ローカル再現ではowner callだけがV1からV2へ進み、outsiderは拒否されました。implementation addressへ直接送ったupgrade callも、proxy contextではないためUUPSUnauthorizedCallContextで拒否されました。
Beacon Proxyはbeaconを経由して二段階で解決する
Beacon Proxyはimplementation slotではなくbeacon slotを読み、そのbeacon contractのimplementation()をcallして実行先を得ます。1つのUpgradeableBeaconを2つのproxyが共有すれば、beaconの1回のupgradeで両方の実行codeが変わります。ただしvalueやownerなどのstateは各proxyに別々に残ります。
OpenZeppelin 5.xのBeaconProxyは効率のためbeaconをimmutableにも保持し、実際のcall pathはimmutable値を使います。implementation logicがbeacon slotを書き換えても実行先は変わらず、raw slotとの不一致を作ります。beacon slot、proxy bytecode / source、beaconのimplementation()を三点照合します。
3方式を同じローカルchainへ固定して再現した
公開再現データは、Foundry / Anvil 1.8.0、Solidity 0.8.36、OpenZeppelin Contracts 5.6.1、viem 2.56.0、Prague EVM、local chain ID 31337へ固定しています。Foundryは12 passed; 0 failed; 0 skippedです。
| path | raw slot / behavior | local result |
|---|---|---|
| Transparent initial | implementation=V1、admin=ProxyAdmin、beacon=zero | owner=deployer、value=41 |
| Transparent compatible V2 | implementationだけV2へ変更 | owner / valueを維持、revision=2 |
| Transparent collision V2 | 同じraw slotを逆の型で読む | owner=0x…0029、valueが旧ownerの整数表現 |
| UUPS | implementation=V1→V2、admin / beacon=zero | owner upgrade成功、outsiderとdirect implementationは拒否 |
| unsafe initializer | 明示的にempty dataを許可したproxy | outsiderがproxy ownerを取得。unsafe implementation自身も直接初期化可能 |
| Beacon A / B | implementation / admin=zero、beacon=共通address | 1回のbeacon upgradeで両方version 2、value 7 / 9を維持 |
このaddressとprivate keyはAnvilが作る使い捨て検証環境専用です。mainnet address、個人key、実在account、wallet prompt、外部RPC、資産の証拠ではありません。
initializerはproxy construction dataで同時に実行する
implementationのconstructorはimplementation自身のstorageにしか作用しません。proxy側のownerや初期値は、initializerをdelegatecallしてproxy storageへ書きます。
安全側の再現codeは次の2点を組み合わせます。
- proxy構築時の
dataへinitialize(owner, value)を入れ、配置と初期化を同じtransactionにする - implementation constructorで
_disableInitializers()を呼び、implementation自身を直接初期化できないようにする
OpenZeppelin ERC1967Proxy 5.6はempty dataを既定でERC1967ProxyUninitializedとして拒否します。事故を再現するため、検証用codeだけが次のunsafe overrideを明示しています。
contract UnsafeUninitializedProxy is ERC1967Proxy {
constructor(address implementation) ERC1967Proxy(implementation, "") {}
function _unsafeAllowUninitialized() internal pure override returns (bool) {
return true;
}
}
このproxyへoutsiderが先にinitializeするとownerを取れました。constructor dataを空にすることやoverrideをproduction patternとして勧める例ではありません。またimplementation直callでownerになれても、それだけでproxy ownerになったとは限りません。2つのstorage contextを分けます。
storage layoutはsource差分ではなくslotの意味を比べる
delegatecallでは新implementationも既存proxy storageを読みます。変数名が同じでも、declaration order、inheritance、packing、型が変われば同じwordの意味が変わります。
ローカル再現のV1とcompatible V2は末尾へrevisionを追加しました。collision V2は最初の2宣言を逆にしています。
| raw position | V1 / compatible V2 | collision V2 | 同じwordの結果 |
|---|---|---|---|
| slot 0 | uint256 value = 41 |
address owner |
0x0000…0029として読む |
| slot 1 | address owner = 0xf39F…2266 |
uint256 value |
1390849295786071768276380950238675083608645509734として読む |
| slot 2 | compatible V2のuint256 revision = 2 |
宣言なし | collision側からは使わない |
upgrade transaction自体は成功しても、意味は壊れています。compilerのstorage layout output、前version、次version、継承順、namespaced storage、migration initializerを比較し、local forkまたは決定的なローカル再現で値を読むまでをupgrade reviewに含めます。
EIP-7702 lifecycleの記事でも、codeの参照先を変えてもstorageやexternal permissionは自動で消えないことを確認しました。仕組みは別ですが、「codeの変更」と「addressに残るstate」を分ける観点はproxy upgradeでも同じです。
同じdelegatecallでもtransaction中だけ存在するstateは別領域です。EIP-1153 transient storageの記事で、CALLはcallee、DELEGATECALLはproxy / host側のtransient storeを所有することと、normal return後のcleanupを再現しています。
upgrade前後の権限や会計を単発scenarioだけでなく操作列として検査する場合は、Foundry invariant testの記事でhandler、actor、ghost variable、縮約counterexampleの作り方を確認できます。
eventとblock-specific slotでimplementation履歴を作る
ERC-1967はslot変更時のUpgraded、AdminChanged、BeaconUpgraded eventを定義します。current slotだけでなく、deployment blockからeventをaddress別・log index順に集め、そのblockのslotを読み直します。
import { parseAbiItem } from 'viem'
const upgraded = await publicClient.getLogs({
address: proxy,
event: parseAbiItem('event Upgraded(address indexed implementation)'),
fromBlock: deploymentBlock,
toBlock: fixedHead,
})
for (const log of upgraded) {
const word = await publicClient.getStorageAt({
address: proxy,
slot: slots.implementation,
blockNumber: log.blockNumber,
})
console.log({ blockNumber: log.blockNumber, logIndex: log.logIndex, event: log.args, word })
}
ローカル再現では次の履歴になりました。
| emitter | event | blocks / count | 読み方 |
|---|---|---|---|
| Transparent proxy | Upgraded |
7、8、9 / 3 | initial V1 → compatible V2 → collision V2 |
| Transparent proxy | AdminChanged |
7 / 1 | zero → generated ProxyAdmin |
| UUPS proxy | Upgraded |
10、11 / 2 | constructor V1 → owner-authorized V2 |
| beacon | Upgraded |
15、18 / 2 | beacon自体のimplementation履歴 |
| Beacon Proxy A / B | BeaconUpgraded |
16、17 / 各1 | 各proxyが参照するbeaconの発見event |
同じUpgraded(address) signatureでも、proxyがemitするimplementation変更とbeacon contractがemitする変更があります。必ずemitter addressを記録します。authority調査ではProxyAdmin / beaconのownership transfer、AccessControl、timelock、governance eventも別に追います。
reorg、RPCの履歴保持範囲、deployment blockの取り違え、custom eventの欠落は残る境界です。重要な調査では複数のarchive-capable sourceとblock hashを照合します。
blob transactionではexecution RPCのversioned hashが残っていても、Beacon sidecarは別のretention境界を持ちます。Fusaka・PeerDASの記事では、固定blockのexecution evidenceとretention-limitedなblob dataを分けて再現しています。
verified sourceは調査の入口であって安全証明ではない
source verificationが示すのは、対象bytecodeとsource / compiler設定の対応です。次は別々に確認します。
- proxy addressのruntime bytecodeとproxy source
- current implementation addressのruntime bytecode、source、ABI
- ProxyAdmin、UUPS authority、beaconとそのownerのsource・current state
- compilerのstorage layoutとupgrade前後の互換性
- deployment / upgrade / authority changeのevent履歴
- pause、role、oracle、external call、business logicなどapplication固有risk
Etherscanのproxy verification APIはproxyとexpected implementationの関連付けを検証するendpointです。Sourcifyもsource metadata照合の別経路になります。どちらもupgrade authorityの妥当性、storage互換性、audit完了、悪意あるlogicがないことを保証しません。
source照合そのものを追試する場合は、verified sourceをcreation/runtime・compiler・constructor・metadataから再ビルドする手順で、proxyとimplementationを別targetにしたraw bytecode比較を確認できます。
Explorerが「Read as Proxy」で結合ABIを表示しても、current block、implementation source、proxy state、admin / beacon authorityを自分の調査メモへ分けます。未検証ならABIを推測してWriteしません。verifiedでもシミュレーションや権限確認を省略しません。
Safe proxyを調べている場合は、ERC-1967 slotがある前提にせず、対象Safe versionの公式proxy sourceへ戻ります。Safe transactionの署名・nonce・実行失敗を分ける手順では、implementationとVERSION()を固定してからowners、threshold、modules、guards、Safe nonceをread-onlyで追います。
read-only調査チェックリスト
- chain、proxy address、fixed block number / hashを記録した
- implementation、admin、beaconのexact slotを同じblockで読んだ
- 32-byte word、下位20-byte address、zero、code有無を保存した
- proxy runtime bytecode / sourceとcall behaviorを照合した
- Transparent、UUPS、Beacon、custom proxyをslot一つだけで断定していない
- beaconなら
beacon.implementation()を同じblockで読んだ - UUPSなら
proxiableUUID、proxy context、_authorizeUpgradeを確認した - admin / owner / role / multisig / timelock / governanceをauthority chainとして追った
-
Upgraded、AdminChanged、BeaconUpgradedをemitterとlog index付きで集めた - event blockのslotを再読し、current valueだけから過去を推測していない
- implementation direct callとproxy delegatecallのstorage contextを分けた
- proxy constructor data、initializer version、implementation lockを確認した
- compiler storage layoutをupgrade前後で比較し、local stateを再現した
- verified sourceを公式性、監査、安全、変更不能と同一視していない
- 調査のread-only操作とwallet接続、署名、upgrade、資産操作を分離した
Proxy調査の完了条件は、Explorerがimplementation名を表示したことではありません。proxy addressのraw slotを起点に、実装code、upgrade authority、event履歴、storage layoutを同じblockへ対応させることです。
この記事にスポンサー、affiliate、wallet接続、署名要求、upgrade transaction、資産操作のCTAはありません。将来Explorer、RPC、monitoring、verification、audit、developer toolingの広告を置く場合も広告であることを明示し、raw slot、source、authority、events、layoutの調査結果や安全評価から分離します。
確認した一次情報
- ERC-1967 Proxy Storage Slots確認日: 2026/08/30
- OpenZeppelin Contracts 5.x Proxy API確認日: 2026/08/30
- OpenZeppelin Upgrades Plugins確認日: 2026/08/30
- Solidity Layout of State Variables in Storage確認日: 2026/08/30
- Ethereum JSON-RPC eth_getStorageAt確認日: 2026/08/30
- viem getStorageAt確認日: 2026/08/30
- Etherscan Verify Proxy Contract API確認日: 2026/08/30
- Sourcify API確認日: 2026/08/30
- Foundry v1.8.0確認日: 2026/08/30



