From e5e6102fe39baa8d29ceeb0becc82289de47b0e1 Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 28 Sep 2026 14:55:11 +0000 Subject: [PATCH 1/4] ci: add the manual verify caller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rainix ships `rainix-manual-sol-verify` for verifying a contract that is already on chain. This repo never wired up a caller, so a deploy that landed but failed verification had no repair path. That is not hypothetical. On 2026-09-28 `decimal-float` deployed to all nine networks and then reported `Not all (0 / 1) contracts were verified!` after forty `Pending in queue` polls. Re-dispatching the deploy cannot fix it: the Zoltu deploy is deterministic and so idempotent, the rerun broadcasts nothing, and `--verify` has nothing to submit. It would go green having verified nothing. `networks` defaults to chain IDs, not names. `--chain` takes either, but foundry has no name for HyperEVM (999) or Robinhood Chain (4663), and this repo's `base_sepolia` alias is rejected outright — foundry's name is `base-sepolia`. Every `[etherscan]` entry carries an explicit `chain`, so an ID resolves the right key and verifier URL for all nine. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/manual-sol-verify.yaml | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/manual-sol-verify.yaml diff --git a/.github/workflows/manual-sol-verify.yaml b/.github/workflows/manual-sol-verify.yaml new file mode 100644 index 0000000..5604150 --- /dev/null +++ b/.github/workflows/manual-sol-verify.yaml @@ -0,0 +1,46 @@ +name: Manual sol verify +# Explorer source verification for a contract ALREADY on chain, run by hand. +# +# `Manual sol artifacts` submits source only for what its own run broadcast, so +# a deploy that lands and then loses the race with an explorer's indexer cannot +# be repaired by re-dispatching it: the Zoltu deploy is deterministic and +# therefore idempotent, the rerun broadcasts nothing, and `--verify` has nothing +# to submit. That is what happened to `DecimalFloat` at +# 0xEc632ea4D04A6D72F87E60FEb4C6B6813cda59bd on 2026-09-28 — deployed to all +# nine networks, then `Not all (0 / 1) contracts were verified!` after forty +# `Pending in queue` polls. +# +# Never broadcasts and takes no deploy key. +on: + workflow_dispatch: + inputs: + contract: + description: "Fully qualified artifact path (path:Contract)" + required: true + type: choice + options: + - src/concrete/DecimalFloat.sol:DecimalFloat + address: + description: "Address to submit source for" + required: true + type: string + default: "0xEc632ea4D04A6D72F87E60FEb4C6B6813cda59bd" + networks: + description: "Whitespace separated forge verify-contract --chain values" + required: true + type: string + # Chain IDs rather than names. `--chain` takes either, but foundry has no + # name for HyperEVM (999) or Robinhood Chain (4663), and `base_sepolia` + # is this repo's `[rpc_endpoints]` alias which `--chain` rejects outright + # (its name is `base-sepolia`). Every `[etherscan]` entry carries an + # explicit `chain`, so an ID resolves the right key and verifier URL for + # all nine — including Robinhood, whose entry points at its Blockscout. + default: "42161 8453 84532 56 1 14 999 137 4663" +jobs: + verify: + uses: rainlanguage/rainix/.github/workflows/rainix-manual-sol-verify.yaml@main + with: + contract: ${{ inputs.contract }} + address: ${{ inputs.address }} + networks: ${{ inputs.networks }} + secrets: inherit From 2ad3bcd9c9e2b77d21e8ce8e60e8c5eec43c38b5 Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 28 Sep 2026 15:00:43 +0000 Subject: [PATCH 2/4] ci: read the verify address and explorers, do not type them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The defaults were a hardcoded address and a hardcoded list of nine chain IDs. Both restate something the repo already states, which is the exact duplication the rest of this branch removed. The address moves whenever the creation code does — it moved to 0xEc632ea4 this week — so a literal here goes stale silently and submits source against whatever used to be at it. The chain list would simply never grow: a network added to `[rpc_endpoints]` and `[etherscan]` is a network nothing submits to, with no failure to say so. Both are now READ in a `resolve` job that runs before the verify: - the address from `src/generated/candidate/.sol`, which the build generates from the creation code the deploy broadcast; - the explorers from `[etherscan]` in `foundry.toml`, whose agreement with `LibRainDeploy.supportedNetworks()` is already asserted by `testSupportedNetworksAreFullyConfigured`, so this list cannot drift from the deploy's without CI failing first. A workflow input cannot read either, which is why it is a job rather than a `default:`. Both reads carry `|| true`, because under `set -e` with `pipefail` a grep that matches nothing exits 1 and would kill the step before the empty checks could name which read came back blank. An empty `networks` would otherwise submit to no explorer and exit 0 having verified nothing. Verified locally against the real files: address resolves to 0xEc632ea4D04A6D72F87E60FEb4C6B6813cda59bd and networks to the nine IDs 42161 8453 84532 1 14 999 4663 56 137. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/manual-sol-verify.yaml | 103 +++++++++++++++++------ 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/.github/workflows/manual-sol-verify.yaml b/.github/workflows/manual-sol-verify.yaml index 5604150..6fedefb 100644 --- a/.github/workflows/manual-sol-verify.yaml +++ b/.github/workflows/manual-sol-verify.yaml @@ -5,9 +5,8 @@ name: Manual sol verify # a deploy that lands and then loses the race with an explorer's indexer cannot # be repaired by re-dispatching it: the Zoltu deploy is deterministic and # therefore idempotent, the rerun broadcasts nothing, and `--verify` has nothing -# to submit. That is what happened to `DecimalFloat` at -# 0xEc632ea4D04A6D72F87E60FEb4C6B6813cda59bd on 2026-09-28 — deployed to all -# nine networks, then `Not all (0 / 1) contracts were verified!` after forty +# to submit. That is what happened to `DecimalFloat` on 2026-09-28 — deployed to +# all nine networks, then `Not all (0 / 1) contracts were verified!` after forty # `Pending in queue` polls. # # Never broadcasts and takes no deploy key. @@ -15,32 +14,88 @@ on: workflow_dispatch: inputs: contract: - description: "Fully qualified artifact path (path:Contract)" + description: "Contract to verify" required: true type: choice options: - - src/concrete/DecimalFloat.sol:DecimalFloat - address: - description: "Address to submit source for" - required: true - type: string - default: "0xEc632ea4D04A6D72F87E60FEb4C6B6813cda59bd" - networks: - description: "Whitespace separated forge verify-contract --chain values" - required: true - type: string - # Chain IDs rather than names. `--chain` takes either, but foundry has no - # name for HyperEVM (999) or Robinhood Chain (4663), and `base_sepolia` - # is this repo's `[rpc_endpoints]` alias which `--chain` rejects outright - # (its name is `base-sepolia`). Every `[etherscan]` entry carries an - # explicit `chain`, so an ID resolves the right key and verifier URL for - # all nine — including Robinhood, whose entry points at its Blockscout. - default: "42161 8453 84532 56 1 14 999 137 4663" + - DecimalFloat + # Only `DecimalFloat` has source to verify. The other deployed artifact + # is the log tables, which is a data contract: `LibDataContract` creation + # code wrapping table BYTES, with no Solidity source an explorer could + # match it against. jobs: + # The address and the explorer list are READ from the repo, never typed into + # this file. Both have a single source already — the committed snapshot and + # `[etherscan]` — and a copy here would be a value that silently goes stale: + # the address moves whenever the creation code does (it moved to + # 0xEc632ea4 this week), and a network added to `[rpc_endpoints]` and + # `[etherscan]` would simply never be submitted to. + # + # A workflow input cannot read either, which is why this is a job that runs + # before the verify rather than a `default:`. + resolve: + runs-on: ubuntu-latest + outputs: + contract: ${{ steps.pins.outputs.contract }} + address: ${{ steps.pins.outputs.address }} + networks: ${{ steps.pins.outputs.networks }} + steps: + - uses: rainlanguage/rainix/.github/actions/checkout@main + - id: pins + env: + CONTRACT: ${{ inputs.contract }} + run: | + set -euo pipefail + + # `|| true` on both reads: under `set -e` with `pipefail` a grep that + # matches nothing exits 1 and kills the step, so the empty checks below + # would never run and the operator would get a bare exit code instead of + # being told which read came back empty. + snapshot="src/generated/candidate/$CONTRACT.sol" + address="$(grep -oE 'DEPLOYED_ADDRESS = address\(0x[0-9a-fA-F]{40}\)' "$snapshot" \ + | grep -oE '0x[0-9a-fA-F]{40}' || true)" + + # Scoped to the `[etherscan]` section: these are the explorers the repo + # is configured to submit to, and `testSupportedNetworksAreFullyConfigured` + # already fails the build when that set and + # `LibRainDeploy.supportedNetworks()` disagree. So this list cannot + # drift from the deploy's network list without CI saying so first. + # + # Chain IDs, not names. `--chain` takes either, but foundry has no name + # for HyperEVM (999) or Robinhood Chain (4663), and this repo's + # `base_sepolia` alias is rejected outright — foundry's name is + # `base-sepolia`. Every entry carries an explicit `chain`, so an ID + # resolves the right key and verifier URL for all of them. + networks="$(sed -n '/^\[etherscan\]/,/^\[/{/^\[/!p}' foundry.toml \ + | grep -oE 'chain = [0-9]+' \ + | grep -oE '[0-9]+' \ + | tr '\n' ' ' || true)" + + # A grep that matches nothing yields an empty string, and the verify + # below would then submit to no explorer and exit 0 having done + # nothing. Fail here instead, naming which read came back empty. + if [ -z "$address" ] + then + echo "::error::no DEPLOYED_ADDRESS in $snapshot" + exit 1 + fi + if [ -z "${networks// /}" ] + then + echo "::error::no [etherscan] entries with a chain id in foundry.toml" + exit 1 + fi + + echo "verifying $CONTRACT at $address on:$networks" + { + echo "contract=src/concrete/$CONTRACT.sol:$CONTRACT" + echo "address=$address" + echo "networks=$networks" + } >> "$GITHUB_OUTPUT" verify: + needs: resolve uses: rainlanguage/rainix/.github/workflows/rainix-manual-sol-verify.yaml@main with: - contract: ${{ inputs.contract }} - address: ${{ inputs.address }} - networks: ${{ inputs.networks }} + contract: ${{ needs.resolve.outputs.contract }} + address: ${{ needs.resolve.outputs.address }} + networks: ${{ needs.resolve.outputs.networks }} secrets: inherit From 19ae01aed26b15c907e73d0c356a7aec861045da Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 28 Sep 2026 15:05:50 +0000 Subject: [PATCH 3/4] ci: let rainix read the explorers rainix#400 removes the `networks` input from `rainix-manual-sol-verify` and reads the explorers from the caller's own `[etherscan]`. So this caller no longer resolves them, and cannot get them wrong. The address stays here, because it is contract specific and rainix has no way to know which snapshot holds it. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/manual-sol-verify.yaml | 55 ++++++------------------ 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/.github/workflows/manual-sol-verify.yaml b/.github/workflows/manual-sol-verify.yaml index 6fedefb..a1a3379 100644 --- a/.github/workflows/manual-sol-verify.yaml +++ b/.github/workflows/manual-sol-verify.yaml @@ -10,6 +10,10 @@ name: Manual sol verify # `Pending in queue` polls. # # Never broadcasts and takes no deploy key. +# +# The explorers are not named here. `rainix-manual-sol-verify` reads them from +# this repo's own `[etherscan]`, the same set `testSupportedNetworksAreFullyConfigured` +# holds to `LibRainDeploy.supportedNetworks()`. on: workflow_dispatch: inputs: @@ -24,21 +28,16 @@ on: # code wrapping table BYTES, with no Solidity source an explorer could # match it against. jobs: - # The address and the explorer list are READ from the repo, never typed into - # this file. Both have a single source already — the committed snapshot and - # `[etherscan]` — and a copy here would be a value that silently goes stale: - # the address moves whenever the creation code does (it moved to - # 0xEc632ea4 this week), and a network added to `[rpc_endpoints]` and - # `[etherscan]` would simply never be submitted to. - # - # A workflow input cannot read either, which is why this is a job that runs - # before the verify rather than a `default:`. + # The address is READ from the committed snapshot, never typed into this file. + # It moves whenever the creation code does — it moved to 0xEc632ea4 this week + # — so a literal here would go stale silently and submit source against + # whatever used to be at it. A workflow input cannot read the snapshot, which + # is why this is a job rather than a `default:`. resolve: runs-on: ubuntu-latest outputs: contract: ${{ steps.pins.outputs.contract }} address: ${{ steps.pins.outputs.address }} - networks: ${{ steps.pins.outputs.networks }} steps: - uses: rainlanguage/rainix/.github/actions/checkout@main - id: pins @@ -47,49 +46,24 @@ jobs: run: | set -euo pipefail - # `|| true` on both reads: under `set -e` with `pipefail` a grep that - # matches nothing exits 1 and kills the step, so the empty checks below - # would never run and the operator would get a bare exit code instead of - # being told which read came back empty. + # `|| true` because under `set -e` with `pipefail` a grep that matches + # nothing exits 1 and kills the step, so the check below would never + # run and the operator would get a bare exit code instead of being told + # the snapshot had no address in it. snapshot="src/generated/candidate/$CONTRACT.sol" address="$(grep -oE 'DEPLOYED_ADDRESS = address\(0x[0-9a-fA-F]{40}\)' "$snapshot" \ | grep -oE '0x[0-9a-fA-F]{40}' || true)" - # Scoped to the `[etherscan]` section: these are the explorers the repo - # is configured to submit to, and `testSupportedNetworksAreFullyConfigured` - # already fails the build when that set and - # `LibRainDeploy.supportedNetworks()` disagree. So this list cannot - # drift from the deploy's network list without CI saying so first. - # - # Chain IDs, not names. `--chain` takes either, but foundry has no name - # for HyperEVM (999) or Robinhood Chain (4663), and this repo's - # `base_sepolia` alias is rejected outright — foundry's name is - # `base-sepolia`. Every entry carries an explicit `chain`, so an ID - # resolves the right key and verifier URL for all of them. - networks="$(sed -n '/^\[etherscan\]/,/^\[/{/^\[/!p}' foundry.toml \ - | grep -oE 'chain = [0-9]+' \ - | grep -oE '[0-9]+' \ - | tr '\n' ' ' || true)" - - # A grep that matches nothing yields an empty string, and the verify - # below would then submit to no explorer and exit 0 having done - # nothing. Fail here instead, naming which read came back empty. if [ -z "$address" ] then echo "::error::no DEPLOYED_ADDRESS in $snapshot" exit 1 fi - if [ -z "${networks// /}" ] - then - echo "::error::no [etherscan] entries with a chain id in foundry.toml" - exit 1 - fi - echo "verifying $CONTRACT at $address on:$networks" + echo "verifying $CONTRACT at $address" { echo "contract=src/concrete/$CONTRACT.sol:$CONTRACT" echo "address=$address" - echo "networks=$networks" } >> "$GITHUB_OUTPUT" verify: needs: resolve @@ -97,5 +71,4 @@ jobs: with: contract: ${{ needs.resolve.outputs.contract }} address: ${{ needs.resolve.outputs.address }} - networks: ${{ needs.resolve.outputs.networks }} secrets: inherit From 225bb0196ac314e51e2de914ea7084162811521d Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 28 Sep 2026 15:08:59 +0000 Subject: [PATCH 4/4] ci: trim the verify comments Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/manual-sol-verify.yaml | 37 ++++++++---------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/.github/workflows/manual-sol-verify.yaml b/.github/workflows/manual-sol-verify.yaml index a1a3379..50c727c 100644 --- a/.github/workflows/manual-sol-verify.yaml +++ b/.github/workflows/manual-sol-verify.yaml @@ -1,19 +1,12 @@ name: Manual sol verify # Explorer source verification for a contract ALREADY on chain, run by hand. # -# `Manual sol artifacts` submits source only for what its own run broadcast, so -# a deploy that lands and then loses the race with an explorer's indexer cannot -# be repaired by re-dispatching it: the Zoltu deploy is deterministic and -# therefore idempotent, the rerun broadcasts nothing, and `--verify` has nothing -# to submit. That is what happened to `DecimalFloat` on 2026-09-28 — deployed to -# all nine networks, then `Not all (0 / 1) contracts were verified!` after forty -# `Pending in queue` polls. +# The deploy is deterministic and so idempotent: re-dispatching `Manual sol +# artifacts` broadcasts nothing and leaves `--verify` nothing to submit, so a +# deploy that lands and then fails verification needs this instead. # -# Never broadcasts and takes no deploy key. -# -# The explorers are not named here. `rainix-manual-sol-verify` reads them from -# this repo's own `[etherscan]`, the same set `testSupportedNetworksAreFullyConfigured` -# holds to `LibRainDeploy.supportedNetworks()`. +# Never broadcasts, takes no deploy key. Explorers come from `[etherscan]`, read +# by rainix. on: workflow_dispatch: inputs: @@ -22,17 +15,13 @@ on: required: true type: choice options: + # The log tables have no Solidity source to match, being a data + # contract wrapping table bytes. - DecimalFloat - # Only `DecimalFloat` has source to verify. The other deployed artifact - # is the log tables, which is a data contract: `LibDataContract` creation - # code wrapping table BYTES, with no Solidity source an explorer could - # match it against. jobs: - # The address is READ from the committed snapshot, never typed into this file. - # It moves whenever the creation code does — it moved to 0xEc632ea4 this week - # — so a literal here would go stale silently and submit source against - # whatever used to be at it. A workflow input cannot read the snapshot, which - # is why this is a job rather than a `default:`. + # The address is read, not typed: it moves with the creation code, and a + # literal would submit source against whatever used to be at it. An input + # cannot read a file, hence a job. resolve: runs-on: ubuntu-latest outputs: @@ -46,10 +35,8 @@ jobs: run: | set -euo pipefail - # `|| true` because under `set -e` with `pipefail` a grep that matches - # nothing exits 1 and kills the step, so the check below would never - # run and the operator would get a bare exit code instead of being told - # the snapshot had no address in it. + # `|| true`: under pipefail a no-match grep exits 1 and would kill the + # step before the check below could say what was missing. snapshot="src/generated/candidate/$CONTRACT.sol" address="$(grep -oE 'DEPLOYED_ADDRESS = address\(0x[0-9a-fA-F]{40}\)' "$snapshot" \ | grep -oE '0x[0-9a-fA-F]{40}' || true)"