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