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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
#!/usr/bin/env bash
#
# Proves the claim the registry exists to make: LOVE lands at the same address
# on two chains that have nothing about their wETH in common.
#
# Two anvil nodes with different chain ids get different wETH implementations,
# at different addresses — node A the OP Stack predeploy, node B canonical
# WETH9 at its mainnet address — using the same runtime bytecode the tests
# etch. Each node is then brought up the way a real chain would be: registry,
# registration, token. If the two token addresses match, wETH is genuinely out
# of the creation code.
#
# The previous version of this script gave both nodes the same wETH, because
# back then the address was a constructor argument and matching was only
# possible when the argument matched. That is exactly what changed.
#
# Usage: ./script/multichain-check.sh
#
# Env overrides: PORT_A, PORT_B, CHAIN_ID_A, CHAIN_ID_B, SALT
set -euo pipefail
PORT_A=${PORT_A:-8545}
PORT_B=${PORT_B:-8546}
CHAIN_ID_A=${CHAIN_ID_A:-31337}
CHAIN_ID_B=${CHAIN_ID_B:-31338}
# First anvil dev account — local only, never use this key anywhere else.
ANVIL_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
CREATE2_FACTORY=0x4e59b44847b379578588920cA78FbF26c0B4956C
# Node A: the OP Stack predeploy. Node B: canonical WETH9, where it sits on
# Ethereum. Different implementations, different addresses, both allowlisted.
WETH_A=0x4200000000000000000000000000000000000006
WETH_B=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
FIXTURE_A="$ROOT/test/fixtures/weth9-op-legacy.hex"
FIXTURE_B="$ROOT/test/fixtures/weth9-canonical.hex"
RPC_A="http://127.0.0.1:$PORT_A"
RPC_B="http://127.0.0.1:$PORT_B"
TMP=$(mktemp -d)
PIDS=()
cleanup() {
for pid in ${PIDS[@]+"${PIDS[@]}"}; do
kill "$pid" 2>/dev/null || true
done
rm -rf "$TMP"
rm -rf "$ROOT/broadcast/Love.s.sol/$CHAIN_ID_A" "$ROOT/broadcast/Love.s.sol/$CHAIN_ID_B"
rmdir "$ROOT/broadcast/Love.s.sol" "$ROOT/broadcast" 2>/dev/null || true
}
trap cleanup EXIT
start_node() {
local port=$1 chain_id=$2 rpc=$3
anvil --port "$port" --chain-id "$chain_id" --silent >"$TMP/anvil-$port.log" 2>&1 &
PIDS+=($!)
for _ in $(seq 1 50); do
if cast chain-id --rpc-url "$rpc" >/dev/null 2>&1; then
return 0
fi
sleep 0.2
done
echo "anvil on port $port never came up:" >&2
cat "$TMP/anvil-$port.log" >&2
exit 1
}
# Puts real wETH runtime bytecode at an address, the way the chain itself would
# have it. The registry checks EXTCODEHASH, so only genuine bytecode passes —
# there is no test-only path through the allowlist.
place_weth() {
local rpc=$1 addr=$2 fixture=$3 label=$4
[[ -f $fixture ]] || {
echo "missing fixture $fixture" >&2
exit 1
}
cast rpc anvil_setCode "$addr" "$(tr -d '\n' <"$fixture")" --rpc-url "$rpc" >/dev/null
if [[ $(cast code "$addr" --rpc-url "$rpc") == "0x" ]]; then
echo "$label: failed to place wETH at $addr" >&2
exit 1
fi
}
deploy() {
local rpc=$1 label=$2 weth=$3
WETH=$weth forge script script/Love.s.sol:LoveScript \
--rpc-url "$rpc" \
--private-key "$ANVIL_KEY" \
--broadcast \
--skip-simulation >"$TMP/deploy-$label.log" 2>&1 ||
{
echo "deploy on $label failed:" >&2
cat "$TMP/deploy-$label.log" >&2
exit 1
}
grep -oE 'predicted +0x[0-9a-fA-F]{40}' "$TMP/deploy-$label.log" | tail -1 | grep -oE '0x[0-9a-fA-F]{40}'
}
report() {
local rpc=$1 addr=$2 label=$3
local code
code=$(cast code "$addr" --rpc-url "$rpc")
if [[ ${#code} -le 2 ]]; then
echo "$label: no code at $addr" >&2
exit 1
fi
printf '%s %s %s bytes %s / %s\n' \
"$label" "$addr" "$(((${#code} - 2) / 2))" \
"$(cast call "$addr" 'name()(string)' --rpc-url "$rpc")" \
"$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")"
printf '%s registry %s\n' "$label" "$(cast call "$addr" 'REGISTRY()(address)' --rpc-url "$rpc")"
printf '%s weth %s\n' "$label" "$(cast call "$addr" 'WETH()(address)' --rpc-url "$rpc")"
}
cd "$ROOT"
for cmd in anvil cast forge; do
command -v "$cmd" >/dev/null || {
echo "$cmd not found — install foundry" >&2
exit 1
}
done
start_node "$PORT_A" "$CHAIN_ID_A" "$RPC_A"
start_node "$PORT_B" "$CHAIN_ID_B" "$RPC_B"
for pair in "A|$RPC_A" "B|$RPC_B"; do
IFS='|' read -r label rpc <<<"$pair"
if [[ $(cast code "$CREATE2_FACTORY" --rpc-url "$rpc") == "0x" ]]; then
echo "$label: deterministic deployer missing at $CREATE2_FACTORY" >&2
exit 1
fi
done
forge build >"$TMP/build.log" 2>&1 || {
cat "$TMP/build.log" >&2
exit 1
}
place_weth "$RPC_A" "$WETH_A" "$FIXTURE_A" A
place_weth "$RPC_B" "$WETH_B" "$FIXTURE_B" B
ADDR_A=$(deploy "$RPC_A" A "$WETH_A")
ADDR_B=$(deploy "$RPC_B" B "$WETH_B")
echo "chain $CHAIN_ID_A — OP Stack legacy WETH9 at $WETH_A"
report "$RPC_A" "$ADDR_A" " A"
echo "chain $CHAIN_ID_B — canonical WETH9 at $WETH_B"
report "$RPC_B" "$ADDR_B" " B"
echo
# macOS ships bash 3.2, which has no ${var,,}.
lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
WETH_ON_A=$(cast call "$ADDR_A" 'WETH()(address)' --rpc-url "$RPC_A")
WETH_ON_B=$(cast call "$ADDR_B" 'WETH()(address)' --rpc-url "$RPC_B")
if [[ $(lower "$WETH_ON_A") == "$(lower "$WETH_ON_B")" ]]; then
echo "FAIL: both chains ended up on the same wETH — this proves nothing" >&2
exit 1
fi
if [[ $(lower "$ADDR_A") != "$(lower "$ADDR_B")" ]]; then
echo "FAIL: addresses differ" >&2
exit 1
fi
echo "OK: same LOVE address on both chains, backed by different wETH"
|