#!/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 }