cre/scripts/fork-e2e.sh275 lines · 19219 bytes
| 1 | #!/usr/bin/env bash |
| 2 | # End to end on a local anvil fork of Ethereum Sepolia: nothing is sent to a live network and no workflow is |
| 3 | # deployed. The stack is deployed on the fork, then the CRE workflow runs in the CRE CLI simulator with --broadcast |
| 4 | # (its reports go through Sepolia's MockKeystoneForwarder on the fork) and every outcome is checked on-chain. |
| 5 | # |
| 6 | # handler 0, log trigger (the Proposed log replayed by tx hash): |
| 7 | # EvilMint -> Vetoed R1, EvilOwner -> Vetoed R2, EvilFee -> Vetoed R3, EvilBaseFee -> Vetoed R9 (BASEFEE) |
| 8 | # EvilDrain (an open rescue(from, to, amount); supply, owner and transfers intact) -> Vetoed R3B, caught by |
| 9 | # the probe that puts the funded holder first |
| 10 | # drain variants A-G (test/fixtures/DrainVariants.sol, each inheriting DemoBTC so only R3B can catch them): |
| 11 | # A reordered args rescue(from, amount, to); B sweep(amount, from, to); C rescueTokens(token, from, to, amount) |
| 12 | # with require(token == this); D rescue gated on the caller holding tokens; E rescue with no Transfer event |
| 13 | # (out-and-back, net zero); F rescue gated on the caller holding ETH; G transferFrom that waives its allowance |
| 14 | # check for a token-holding caller -> every one Vetoed R3B (the simulation funds the stranger with tokens and |
| 15 | # ETH, probes new selectors with the holder in each argument slot, and reads balances after each placement probe) |
| 16 | # honest v2 -> Cleared by CRE, executes after the cooldown |
| 17 | # owner proposes removing the receiver as guardian -> Vetoed R7 (the approver is a separate key) |
| 18 | # the approver pre-approves a shorter delay, the owner proposes it -> Cleared |
| 19 | # handler 1, cron sweeper (the simulator runs a cron handler once per invocation): |
| 20 | # an honest upgrade nobody judged cannot execute after the cooldown (NotCleared); one sweeper run clears it |
| 21 | # the approver clears two proposals by hand and their logs are missed: the sweeper still judges both (an |
| 22 | # approver clearance is not a CRE verdict, AirlockAdmin.oracleJudged): EvilDrain -> Vetoed R3B (the veto wins |
| 23 | # over the clearance), a longer delay -> CRE Clear, clearanceOf keeps the approver's evidence |
| 24 | # an upgrade with ~6 KB of init data -> one sweeper run vetoes it R8 (its Proposed log is over CRE's 5 kb log |
| 25 | # event limit, so in a deployment only the sweeper sees it; the simulator does not enforce that limit) |
| 26 | # every verdict's evidenceHash is recomputed from getProposal alone (veto/tools/check-evidence.ts) |
| 27 | # maxCalls 150 (the live Sepolia value) splits the demo token's probes over two eth_simulateV1 requests per provider. |
| 28 | # |
| 29 | # Env: CRE_SIM simulator binary (default `cre`, which needs `cre login`; a login-free build of the same CLI works) |
| 30 | # SEPOLIA_RPC fork source (default publicnode) |
| 31 | # VETO_ANVIL_PORT (default 19611), KEEP_FORK=1 leaves the anvil fork running at the end |
| 32 | # Keys: anvil's public test mnemonic only: 0 owner/deployer, 2 attester, 3 report sender, 4 approver, |
| 33 | # 5 emergency guardian. Nothing secret. |
| 34 | set -euo pipefail |
| 35 | cd "$(dirname "$0")/.." # cre/ |
| 36 | ROOT=$(cd .. && pwd) |
| 37 | export PATH="$PWD/.tools/bin:$PWD/.tools/node_modules/.bin:$HOME/.foundry/bin:$PATH" |
| 38 | SIM=${CRE_SIM:-cre} |
| 39 | PORT=${VETO_ANVIL_PORT:-19611} |
| 40 | RPC=http://127.0.0.1:$PORT |
| 41 | SRC=${SEPOLIA_RPC:-https://ethereum-sepolia-rpc.publicnode.com} |
| 42 | M="test test test test test test test test test test test junk" |
| 43 | key() { cast wallet private-key --mnemonic "$M" --mnemonic-index "$1"; } |
| 44 | addr() { cast wallet address --private-key "$(key "$1")"; } |
| 45 | DPK=$(key 0) |
| 46 | OWNER=$(addr 0) |
| 47 | export VETO_ATTESTER_KEY_ALL=$(key 2) |
| 48 | export CRE_ETH_PRIVATE_KEY=$(key 3) |
| 49 | APPROVER=$(addr 4) |
| 50 | EMERGENCY=$(addr 5) |
| 51 | export VETO_ANVIL_PORT=$PORT |
| 52 | OUT=$(mktemp -d "${TMPDIR:-/tmp}/airlock-fork-e2e.XXXXXX") |
| 53 | echo "evidence dir: $OUT" |
| 54 | |
| 55 | anvil --fork-url "$SRC" --port "$PORT" --silent >"$OUT/anvil.log" 2>&1 & |
| 56 | ANVIL=$! |
| 57 | echo "$ANVIL" >"$OUT/anvil.pid" |
| 58 | if [ "${KEEP_FORK:-0}" = 1 ]; then echo "anvil fork pid $ANVIL on $RPC (KEEP_FORK=1: stop it with kill $ANVIL)" |
| 59 | else trap 'kill $ANVIL 2>/dev/null || true' EXIT; fi |
| 60 | for _ in $(seq 1 60); do cast chain-id --rpc-url "$RPC" >/dev/null 2>&1 && break; sleep 1; done |
| 61 | [ "$(cast chain-id --rpc-url "$RPC")" = 11155111 ] || { echo "fork is not Sepolia" >&2; exit 1; } |
| 62 | |
| 63 | # Deploy. Sepolia runs the Amsterdam rules (contract creation costs ~7x Osaka): the node estimates gas |
| 64 | # (--skip-simulation). Build output and broadcast logs stay in $OUT; the JSON goes through cache/ (foundry |
| 65 | # fs_permissions) under a per-run name and is moved out. |
| 66 | DJSON="cache/airlock-fork-e2e.$$.json" |
| 67 | # The simulator writes through the permissionless MockKeystoneForwarder (the attester alone authenticates a verdict): |
| 68 | # DeployEth refuses it without ALLOW_MOCK_FORWARDER=1, and refuses an attester shared with another role. |
| 69 | ( cd "$ROOT" && FOUNDRY_PROFILE=eth FOUNDRY_BROADCAST="$OUT/broadcast" DEPLOYER_PK=$DPK ATTESTER_ADDR=$(addr 2) \ |
| 70 | APPROVER_ADDR=$APPROVER EMERGENCY_GUARDIAN_ADDR=$EMERGENCY ALLOW_MOCK_FORWARDER=1 OUT_JSON=$DJSON \ |
| 71 | forge script script/DeployEth.s.sol --rpc-url "$RPC" --broadcast --slow --skip-simulation \ |
| 72 | --out "$OUT/forge-out" --cache-path "$OUT/forge-cache" >"$OUT/deploy.log" 2>&1 ) \ |
| 73 | || { tail -30 "$OUT/deploy.log"; exit 1; } |
| 74 | mv "$ROOT/$DJSON" "$OUT/deployment.json" |
| 75 | D="$OUT/deployment.json" |
| 76 | GA=$(jq -r .airlockAdmin "$D"); RX=$(jq -r .receiver "$D"); PROXY=$(jq -r .proxy "$D"); DELAY=$(jq -r .delayBlocks "$D") |
| 77 | echo "AirlockAdmin $GA AirlockReceiver $RX proxy $PROXY delay $DELAY blocks" |
| 78 | jq --arg rpc "$RPC" '{chainSelectorName:"ethereum-testnet-sepolia", chainId, airlockAdmin, receiver, proxy, holders, |
| 79 | watch:[.owner,.airlockAdmin], rpcs:[{url:$rpc,label:"fork-a"},{url:$rpc,label:"fork-b"},{url:$rpc,label:"fork-c"}], |
| 80 | minAgree:2, strict:false, callGas:1000000, readGas:200000, maxCalls:150, simRequests:2, allowedOpcodes:[], |
| 81 | gasLimit:"2000000", reportGraceBlocks:50, logConfidence:"LATEST", sweepSchedule:"0 * * * * *", sweepWindow:20}' \ |
| 82 | "$D" > veto/config.fork.json |
| 83 | cp veto/config.fork.json "$OUT/config.fork.json" |
| 84 | # Compile the workflow once (TypeScript typecheck + WASM); every simulator run below reuses the binary. |
| 85 | "$SIM" workflow build ./veto -T sepolia-fork -o "$OUT/airlock.wasm" >"$OUT/build.log" 2>&1 || { tail -30 "$OUT/build.log"; exit 1; } |
| 86 | |
| 87 | ZERO=0x0000000000000000000000000000000000000000000000000000000000000000 |
| 88 | IMPL_SLOT=0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc |
| 89 | fail=0 |
| 90 | ok() { echo "ok $*"; } |
| 91 | bad() { echo "FAIL $*"; fail=1; } |
| 92 | rule_hash() { cast keccak "GUARD/$1"; } |
| 93 | status_of() { cast call "$GA" "getProposal(uint256)((uint8,uint8,address,address,uint256,bytes32,bytes32,uint64,uint64,uint256))" "$1" --rpc-url "$RPC" | sed -E 's/^\(([0-9]+), ([0-9]+),.*/\2/'; } |
| 94 | clearance_of() { cast call "$GA" "clearanceOf(uint256)(bytes32)" "$1" --rpc-url "$RPC"; } |
| 95 | judged_of() { cast call "$GA" "oracleJudged(uint256)(bool)" "$1" --rpc-url "$RPC"; } |
| 96 | last_id() { echo $(( $(cast call "$GA" "nextId()(uint256)" --rpc-url "$RPC") - 1 )); } |
| 97 | # "action ruleHash evidenceHash" of the newest AirlockReceiver.Verdict for proposal $1 (empty if none) |
| 98 | verdict_of() { |
| 99 | cast logs --from-block "$(jq -r .startBlock "$D")" --to-block latest --address "$RX" \ |
| 100 | "Verdict(uint256 indexed,uint8 indexed,bytes32,bytes32,bytes32)" "$(cast to-uint256 "$1")" --rpc-url "$RPC" --json \ |
| 101 | | jq -r 'last // empty | "\(.topics[2] | ltrimstr("0x") | sub("^0+";"") | if . == "" then "0" else . end) 0x\(.data[2:66]) 0x\(.data[66:130])"' |
| 102 | } |
| 103 | user_logs() { grep -E '\[USER LOG\]' "$1" | sed 's/^.*\[USER LOG\] / | /' | cut -c1-220; } |
| 104 | simulate() { # name trigger-index [tx-hash] |
| 105 | local name=$1 idx=$2 tx=${3:-} |
| 106 | local args=(workflow simulate ./veto -T sepolia-fork --non-interactive --trigger-index "$idx" --broadcast --wasm "$OUT/airlock.wasm") |
| 107 | [ -n "$tx" ] && args+=(--evm-tx-hash "$tx" --evm-event-index 0) |
| 108 | if ! "$SIM" "${args[@]}" >"$OUT/sim-$name.log" 2>&1; then |
| 109 | bad "$name: simulator exited non-zero"; tail -25 "$OUT/sim-$name.log"; return 1 |
| 110 | fi |
| 111 | user_logs "$OUT/sim-$name.log" |
| 112 | } |
| 113 | propose() { # name, then the cast send signature and args; prints "id tx" |
| 114 | local name=$1; shift |
| 115 | local tx; tx=$(cast send "$GA" "$@" --private-key "$DPK" --rpc-url "$RPC" --json | jq -r .transactionHash) |
| 116 | echo "$name $(last_id) $tx" >>"$OUT/cases.txt" |
| 117 | echo "$(last_id) $tx" |
| 118 | } |
| 119 | expect_veto() { # name id rule |
| 120 | local v; v=$(verdict_of "$2"); local st; st=$(status_of "$2") |
| 121 | read -r act rh ev <<<"${v:-x x x}" |
| 122 | if [ "$st" = 2 ] && [ "$act" = 0 ] && [ "$rh" = "$(rule_hash "$3")" ]; then ok "$1: #$2 Vetoed by CRE with $3 (evidence $ev)" |
| 123 | else bad "$1: #$2 status $st verdict '${v:-none}', expected Vetoed $3"; fi |
| 124 | echo "$2" >>"$OUT/vetoed.txt" |
| 125 | } |
| 126 | expect_clear() { # name id |
| 127 | local v; v=$(verdict_of "$2"); local st; st=$(status_of "$2"); local c; c=$(clearance_of "$2") |
| 128 | read -r act rh ev <<<"${v:-x x x}" |
| 129 | if [ "$st" = 1 ] && [ "$act" = 1 ] && [ "$rh" = "$ZERO" ] && [ "$c" = "$ev" ] && [ "$c" != "$ZERO" ]; then ok "$1: #$2 Cleared by CRE (clearanceOf = evidence $c)" |
| 130 | else bad "$1: #$2 status $st clearance $c verdict '${v:-none}', expected Cleared"; fi |
| 131 | } |
| 132 | log_case() { # name impl rule |
| 133 | local r; r=$(propose "$1" "proposeUpgrade(address,bytes)" "$2" 0x); read -r id tx <<<"$r" |
| 134 | echo "-- $1: proposal #$id (log trigger)" |
| 135 | simulate "$1" 0 "$tx" || return 0 |
| 136 | expect_veto "$1" "$id" "$3" |
| 137 | } |
| 138 | # forge create one of the test-only drain fixtures on the fork and print its address. |
| 139 | create_fixture() { # Contract -> deployed address |
| 140 | ( cd "$ROOT" && FOUNDRY_PROFILE=eth forge create "test/fixtures/DrainVariants.sol:$1" \ |
| 141 | --rpc-url "$RPC" --private-key "$DPK" --broadcast \ |
| 142 | --out "$OUT/forge-out" --cache-path "$OUT/forge-cache" --json 2>>"$OUT/create.log" | jq -r .deployedTo ) |
| 143 | } |
| 144 | # Deploy a drain variant, propose an upgrade to it, and expect it Vetoed R3B with every other rule passing. |
| 145 | variant_case() { # label Contract |
| 146 | local label=$1 contract=$2 impl |
| 147 | impl=$(create_fixture "$contract") |
| 148 | if [ -z "$impl" ] || [ "$impl" = null ]; then bad "$label: forge create $contract failed"; tail -5 "$OUT/create.log"; return 0; fi |
| 149 | echo "$label $impl" >>"$OUT/variants.txt" |
| 150 | echo "-- $label: $contract at $impl (log trigger)" |
| 151 | local r id tx; r=$(propose "$label" "proposeUpgrade(address,bytes)" "$impl" 0x); read -r id tx <<<"$r" |
| 152 | simulate "$label" 0 "$tx" || return 0 |
| 153 | expect_veto "$label" "$id" R3B_THIRD_PARTY_FUNDS |
| 154 | if grep -qE 'R1_TOTAL_SUPPLY|R2_OWNER|R3_TRANSFER_CONSERVATION|R4_TRANSFER_LIVENESS|R9_NEW_OPCODES' <(grep 'simulation veto' "$OUT/sim-$label.log"); then |
| 155 | bad "$label: another rule failed too (expected R3B alone)" |
| 156 | else ok "$label: supply, owner, transfers and opcodes all pass; only R3B fails"; fi |
| 157 | } |
| 158 | |
| 159 | # ---- handler 0: log trigger |
| 160 | log_case evil-mint "$(jq -r .evilMint "$D")" R1_TOTAL_SUPPLY |
| 161 | log_case evil-owner "$(jq -r .evilOwner "$D")" R2_OWNER |
| 162 | log_case evil-fee "$(jq -r .evilFee "$D")" R3_TRANSFER_CONSERVATION |
| 163 | log_case evil-basefee "$(jq -r .evilBaseFee "$D")" R9_NEW_OPCODES |
| 164 | if grep -q 'R9_NEW_OPCODES:.*found: BASEFEE' "$OUT/sim-evil-basefee.log"; then ok "evil-basefee: R9 names the BASEFEE opcode" |
| 165 | else bad "evil-basefee: the simulation log does not name BASEFEE"; fi |
| 166 | RESCUE=$(cast sig "rescue(address,address,uint256)") |
| 167 | log_case evil-drain "$(jq -r .evilDrain "$D")" R3B_THIRD_PARTY_FUNDS |
| 168 | # a placement probe with the holder in slot 0 (from) drains rescue; its effects include moved-others-funds |
| 169 | if grep -qE "$RESCUE\(h0,[^)]*\):[a-z+-]*moved-others-funds" "$OUT/sim-evil-drain.log"; then |
| 170 | ok "evil-drain: the culprit is rescue $RESCUE, caught by a holder-in-slot-0 placement probe" |
| 171 | else bad "evil-drain: the simulation log does not name a rescue $RESCUE(h0,...):moved-others-funds culprit"; fi |
| 172 | if grep -qE 'R1_TOTAL_SUPPLY|R2_OWNER|R3_TRANSFER_CONSERVATION|R4_TRANSFER_LIVENESS|R9_NEW_OPCODES' <(grep 'simulation veto' "$OUT/sim-evil-drain.log"); then |
| 173 | bad "evil-drain: another rule failed too (expected R3B alone)" |
| 174 | else ok "evil-drain: supply, owner, transfers and opcodes all pass; only R3B fails"; fi |
| 175 | if grep -q 'in 2 request(s)' "$OUT/sim-evil-drain.log"; then ok "simulation: the probes went out as two eth_simulateV1 requests per provider" |
| 176 | else bad "simulation: expected two eth_simulateV1 requests per provider at maxCalls 150"; fi |
| 177 | |
| 178 | # ---- handler 0: the reviewer's R3B evasions. Each must still end Vetoed R3B, with every other rule passing. |
| 179 | variant_case variant-a VariantA_RescueReorder |
| 180 | variant_case variant-b VariantB_SweepReorder |
| 181 | variant_case variant-c VariantC_RescueTokensGuard |
| 182 | variant_case variant-d VariantD_RescueRequireTokens |
| 183 | variant_case variant-e VariantE_RescueNoLog |
| 184 | variant_case variant-f VariantF_RescueRequireEth |
| 185 | variant_case variant-g VariantG_TransferFromAllowanceSkip |
| 186 | |
| 187 | # ---- request sizes against the CRE HTTP limits: DemoBTCV2 (honest) and the largest variant (4-arg rescueTokens) |
| 188 | echo "-- request sizes (DemoBTCV2 and the largest variant) vs the 120 kb request / 15 HTTP-call limits" |
| 189 | VC=$(awk '$1=="variant-c"{print $2}' "$OUT/variants.txt" 2>/dev/null) |
| 190 | if [ -n "$VC" ]; then |
| 191 | ( cd veto && bun tools/measure-requests.ts "$RPC" "$D" "$OUT/config.fork.json" "DemoBTCV2=$(jq -r .v2 "$D")" "variant-c=$VC" ) | sed 's/^/ /' \ |
| 192 | || bad "measure: a request exceeds a CRE HTTP size limit" |
| 193 | else bad "measure: variant-c was not deployed"; fi |
| 194 | |
| 195 | read -r V2ID tx <<<"$(propose honest-v2 "proposeUpgrade(address,bytes)" "$(jq -r .v2 "$D")" 0x)" |
| 196 | echo "-- honest-v2: proposal #$V2ID (log trigger)" |
| 197 | simulate honest-v2 0 "$tx" && expect_clear honest-v2 "$V2ID" |
| 198 | |
| 199 | # Nobody judges this one: no log-trigger run. |
| 200 | read -r LATEID _ <<<"$(propose unjudged-v2 "proposeUpgrade(address,bytes)" "$(jq -r .v2 "$D")" 0x)" |
| 201 | echo "-- unjudged-v2: proposal #$LATEID (no workflow run)" |
| 202 | |
| 203 | read -r GID tx <<<"$(propose drop-receiver "proposeSetGuardian(address,bool)" "$RX" false)" |
| 204 | echo "-- drop-receiver: proposal #$GID removes the receiver as guardian (log trigger)" |
| 205 | simulate drop-receiver 0 "$tx" && expect_veto drop-receiver "$GID" R7_CONFIG_GUARD |
| 206 | |
| 207 | # The approver (its own key) pre-approves keccak256(abi.encode(uint8 kind=2, address 0, uint256 20)). |
| 208 | cast send "$GA" "approveCodehash(bytes32)" "$(cast keccak "$(cast abi-encode "f(uint8,address,uint256)" 2 0x0000000000000000000000000000000000000000 20)")" \ |
| 209 | --private-key "$(key 4)" --rpc-url "$RPC" >/dev/null |
| 210 | read -r DID tx <<<"$(propose approved-delay "proposeSetDelay(uint64)" 20)" |
| 211 | echo "-- approved-delay: proposal #$DID shortens the delay to 20 blocks, pre-approved by the approver (log trigger)" |
| 212 | simulate approved-delay 0 "$tx" && expect_clear approved-delay "$DID" |
| 213 | |
| 214 | # ---- the cooldown passes |
| 215 | cast rpc anvil_mine "$(printf '0x%x' "$DELAY")" --rpc-url "$RPC" >/dev/null |
| 216 | if cast send "$GA" "execute(uint256)" "$V2ID" --private-key "$DPK" --rpc-url "$RPC" >/dev/null 2>&1 \ |
| 217 | && [ "$(cast storage "$PROXY" "$IMPL_SLOT" --rpc-url "$RPC" | cast parse-bytes32-address)" = "$(cast to-check-sum-address "$(jq -r .v2 "$D")")" ]; then |
| 218 | ok "honest-v2: #$V2ID executed after $DELAY blocks, the proxy now runs v2" |
| 219 | else bad "honest-v2: #$V2ID did not execute"; fi |
| 220 | NOT_CLEARED=$(cast sig "NotCleared(uint256)") |
| 221 | if err=$(cast call "$GA" "execute(uint256)" "$LATEID" --from "$OWNER" --rpc-url "$RPC" 2>&1); then bad "unjudged-v2: #$LATEID would execute without a clearance" |
| 222 | elif grep -q "$NOT_CLEARED" <<<"$err"; then ok "unjudged-v2: #$LATEID cannot execute after the cooldown (NotCleared $NOT_CLEARED)" |
| 223 | else bad "unjudged-v2: #$LATEID execute reverted for another reason: $(tail -1 <<<"$err")"; fi |
| 224 | |
| 225 | # ---- handler 1: the sweeper |
| 226 | echo "-- sweeper run 1" |
| 227 | if simulate sweep-1 1; then |
| 228 | expect_clear unjudged-v2 "$LATEID" |
| 229 | if cast send "$GA" "execute(uint256)" "$LATEID" --private-key "$DPK" --rpc-url "$RPC" >/dev/null 2>&1; then ok "unjudged-v2: #$LATEID executes once the sweeper cleared it" |
| 230 | else bad "unjudged-v2: #$LATEID still cannot execute"; fi |
| 231 | fi |
| 232 | |
| 233 | # The approver clears two proposals by hand, and the log trigger never runs for them. |
| 234 | MANUAL=$(cast keccak "approver: manual review") |
| 235 | read -r ADID _ <<<"$(propose approver-drain "proposeUpgrade(address,bytes)" "$(jq -r .evilDrain "$D")" 0x)" |
| 236 | read -r ACID _ <<<"$(propose approver-delay "proposeSetDelay(uint64)" 30)" |
| 237 | for id in "$ADID" "$ACID"; do cast send "$GA" "clear(uint256,bytes32)" "$id" "$MANUAL" --private-key "$(key 4)" --rpc-url "$RPC" >/dev/null; done |
| 238 | echo "-- approver-drain #$ADID (EvilDrain) and approver-delay #$ACID (delay 30) cleared by the approver, logs missed" |
| 239 | if [ "$(clearance_of "$ADID")" = "$MANUAL" ] && [ "$(judged_of "$ADID")" = false ] && [ "$(judged_of "$ACID")" = false ]; then |
| 240 | ok "approver: #$ADID and #$ACID carry the approver's clearance, oracleJudged false" |
| 241 | else bad "approver: unexpected state before the sweep (clearance $(clearance_of "$ADID"), oracleJudged $(judged_of "$ADID")/$(judged_of "$ACID"))"; fi |
| 242 | echo "-- sweeper run 2" |
| 243 | if simulate sweep-2 1; then |
| 244 | if grep -q "pending without a workflow verdict: #$ADID(approver-cleared) #$ACID(approver-cleared)\$" "$OUT/sim-sweep-2.log"; then |
| 245 | ok "sweeper: run 2 picked up exactly the two approver-cleared proposals" |
| 246 | else bad "sweeper: run 2 did not list exactly #$ADID and #$ACID as approver-cleared candidates"; fi |
| 247 | expect_veto approver-drain "$ADID" R3B_THIRD_PARTY_FUNDS |
| 248 | [ "$(judged_of "$ADID")" = true ] && ok "approver-drain: #$ADID oracleJudged after the CRE veto" || bad "approver-drain: #$ADID oracleJudged still false" |
| 249 | v=$(verdict_of "$ACID"); read -r act rh ev <<<"${v:-x x x}" |
| 250 | if [ "$(status_of "$ACID")" = 1 ] && [ "$act" = 1 ] && [ "$(judged_of "$ACID")" = true ] && [ "$(clearance_of "$ACID")" = "$MANUAL" ] && [ "$ev" != "$MANUAL" ]; then |
| 251 | ok "approver-delay: #$ACID CRE Clear (evidence $ev) on top of the approver's; clearanceOf keeps the first, oracleJudged true" |
| 252 | else bad "approver-delay: #$ACID status $(status_of "$ACID") verdict '${v:-none}' oracleJudged $(judged_of "$ACID") clearance $(clearance_of "$ACID")"; fi |
| 253 | fi |
| 254 | |
| 255 | BIG=0x$(printf '%*s' 12288 '' | tr ' ' 'a') # 6144 bytes of 0xaa |
| 256 | read -r BIGID _ <<<"$(propose big-init "proposeUpgrade(address,bytes)" "$(jq -r .v2 "$D")" "$BIG")" |
| 257 | echo "-- big-init: proposal #$BIGID with $(( (${#BIG} - 2) / 2 )) bytes of init data (no log-trigger run)" |
| 258 | echo "-- sweeper run 3" |
| 259 | simulate sweep-3 1 && expect_veto big-init "$BIGID" R8_INIT_DATA |
| 260 | if grep -q "pending without a workflow verdict: #$BIGID\$" "$OUT/sim-sweep-3.log"; then ok "sweeper: run 3 found only #$BIGID to judge" |
| 261 | else bad "sweeper: run 3 judged more than #$BIGID"; fi |
| 262 | |
| 263 | # ---- a vetoed proposal never executes |
| 264 | cast rpc anvil_mine "$(printf '0x%x' "$DELAY")" --rpc-url "$RPC" >/dev/null |
| 265 | VETOED=$(cast sig "ProposalVetoed(uint256)") |
| 266 | for id in $(cat "$OUT/vetoed.txt" 2>/dev/null); do |
| 267 | if err=$(cast call "$GA" "execute(uint256)" "$id" --from "$OWNER" --rpc-url "$RPC" 2>&1); then bad "#$id would execute although vetoed" |
| 268 | elif grep -q "$VETOED" <<<"$err"; then ok "#$id execute() reverts ProposalVetoed after the cooldown" |
| 269 | else bad "#$id execute reverted for another reason: $(tail -1 <<<"$err")"; fi |
| 270 | done |
| 271 | |
| 272 | echo "-- evidence" |
| 273 | ( cd veto && bun tools/check-evidence.ts "$RPC" "$D" ) | sed 's/^/ /' || bad "evidence: a verdict's evidenceHash does not match its proposal" |
| 274 | |
| 275 | [ $fail = 0 ] && echo "FORK E2E: ALL PASS (logs in $OUT)" || { echo "FORK E2E: FAILURES (logs in $OUT)"; exit 1; } |