julien/lovepublic Fork 0
2f6da5ed88291bee2a94f4f8c2b7354da373f4ba
Commits
Clone
git clone https://git.rickub.com/julien/love.git
git clone ssh://git@rickub.com/julien/love.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

weth-codehashes.sh · 163 lines · 5.3 KBBash Blame HistoryRaw
Merge 5-weth-bytecode-verification into main 2f6da5e rickub 6h ago1#!/usr/bin/env bash
2#
3# Surveys the wETH deployment on every chain in weth-chains.tsv and groups them
4# by implementation, to answer the question the allowlist in
5# src/WETHRegistry.sol depends on: how many distinct wETH implementations are
6# actually out there?
7#
8# For each chain it reports three things:
9#
10# codehash keccak256 of the runtime code — exactly what EXTCODEHASH returns
11# and what the registry compares against, so these values are the
12# allowlist entries verbatim.
13# family the same hash with solc's trailing metadata blob removed. That
14# blob is never executed and embeds a hash of the source path and
15# comments, so one implementation compiled twice gets two
16# codehashes but one family. Grouping by family is how the exact
17# hashes get reviewed together; the registry itself never strips.
18# kind whether the code carries the ERC-20 and wrapper entrypoints
19# itself. A bridged-ETH token has no deposit/withdraw, and a proxy
20# has no entrypoints at all — its codehash commits to a forwarder,
21# not to behaviour, so it must never be allowlisted.
22#
23# Chains are read with eth_getCode and hashed locally rather than with
24# eth_getProof, which several public RPCs do not serve.
25#
26# Usage: ./script/weth-codehashes.sh [chains.tsv]
27#
28# Exits non-zero if any row is unreachable, so a partial survey cannot be
29# mistaken for a clean one.
30
31set -uo pipefail
32
33ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
34CHAINS=${1:-$ROOT/script/weth-chains.tsv}
35
36for cmd in cast; do
37 command -v "$cmd" >/dev/null || {
38 echo "$cmd not found — install foundry" >&2
39 exit 1
40 }
41done
42
43[[ -f $CHAINS ]] || {
44 echo "no chain table at $CHAINS" >&2
45 exit 1
46}
47
48# ERC-20 plus the two wrapper entrypoints. Code missing any of these is not a
49# wETH this project can peg to, whatever it is called.
50SELECTORS=(
51 d0e30db0 # deposit()
52 2e1a7d4d # withdraw(uint256)
53 a9059cbb # transfer(address,uint256)
54 70a08231 # balanceOf(address)
55 095ea7b3 # approve(address,uint256)
56 23b872dd # transferFrom(address,address,uint256)
57)
58
59# Drops solc's metadata blob: the last two bytes hold its length, and the blob
60# itself starts with a CBOR map (0xa1, 0xa2, …). Anything that fails those two
61# sanity checks is left whole rather than guessed at.
62strip_metadata() {
63 local hex=$1 len total marker
64 total=$((${#hex} / 2))
65 [[ ${#hex} -gt 4 ]] || {
66 printf '%s' "$hex"
67 return
68 }
69
70 len=$((16#${hex: -4}))
71 if ((len + 2 >= total || len == 0)); then
72 printf '%s' "$hex"
73 return
74 fi
75
76 marker=${hex:$((${#hex} - (len + 2) * 2)):2}
77 if [[ $marker != a* ]]; then
78 printf '%s' "$hex"
79 return
80 fi
81
82 printf '%s' "${hex:0:$((${#hex} - (len + 2) * 2))}"
83}
84
85rows=()
86unreachable=0
87
88printf '%-16s %-9s %-8s %-12s %s\n' CHAIN ID BYTES KIND CODEHASH
89
90while IFS=$'\t' read -r chain_id name weth rpc; do
91 [[ -z ${chain_id:-} || $chain_id == \#* ]] && continue
92
93 code=$(cast code "$weth" --rpc-url "$rpc" 2>/dev/null)
94
95 if [[ ! $code == 0x* || ${#code} -le 4 ]]; then
96 printf '%-16s %-9s %-8s %-12s %s\n' "$name" "$chain_id" "-" "-" "UNREACHABLE or no code"
97 unreachable=$((unreachable + 1))
98 continue
99 fi
100
101 hex=${code#0x}
102 body=$(strip_metadata "$hex")
103
104 codehash=$(cast keccak "0x$hex")
105 family=$(cast keccak "0x$body")
106
107 kind=wrapper
108 for selector in "${SELECTORS[@]}"; do
109 [[ $body == *"$selector"* ]] || {
110 kind=not-wETH
111 break
112 }
113 done
114
115 printf '%-16s %-9s %-8s %-12s %s\n' "$name" "$chain_id" "$((${#hex} / 2))" "$kind" "$codehash"
116 rows+=("$family|$codehash|$name|$kind|$((${#hex} / 2))")
117done <"$CHAINS"
118
119echo
120echo "families — one block per implementation, indented lines are the allowlist entries"
121echo
122
123wrapper_chains=0
124wrapper_hashes=0
125wrapper_families=0
126
127for family in $(printf '%s\n' ${rows[@]+"${rows[@]}"} | cut -d'|' -f1 | sort -u); do
128 block=$(printf '%s\n' ${rows[@]+"${rows[@]}"} | grep "^$family|")
129 kinds=$(printf '%s\n' "$block" | cut -d'|' -f4 | sort -u | paste -sd, -)
130 size=$(printf '%s\n' "$block" | head -1 | cut -d'|' -f5)
131 chains=$(printf '%s\n' "$block" | wc -l | tr -d ' ')
132
133 printf '%s %s bytes %s chain(s) %s\n' "${family:0:18}" "$size" "$chains" "$kinds"
134
135 for codehash in $(printf '%s\n' "$block" | cut -d'|' -f2 | sort -u); do
136 names=$(printf '%s\n' "$block" | grep "|$codehash|" | cut -d'|' -f3 | sort | paste -sd' ' -)
137 printf ' %s %s\n' "$codehash" "$names"
138
139 if [[ $kinds == wrapper ]]; then
140 wrapper_hashes=$((wrapper_hashes + 1))
141 fi
142 done
143
144 if [[ $kinds == wrapper ]]; then
145 wrapper_families=$((wrapper_families + 1))
146 wrapper_chains=$((wrapper_chains + chains))
147 fi
148
149 echo
150done
151
152printf '%s chains read, %s unreachable\n' "${#rows[@]}" "$unreachable"
153printf 'wrappers: %s chains, %s families, %s exact codehashes to allowlist\n' \
154 "$wrapper_chains" "$wrapper_families" "$wrapper_hashes"
155echo
156echo "A wrapper family is a candidate, not an entry — read its source before"
157echo "adding the hash. Proxies and bridged-ETH tokens show up as not-wETH."
158
159[[ $unreachable -eq 0 ]] || {
160 echo >&2
161 echo "$unreachable row(s) unreachable — survey is incomplete" >&2
162 exit 1
163}