Skip to content

fix(deploy): verify the supply against the DECLARED mint, not against the operator (#407) - #417

Merged
jhfnetboy merged 3 commits into
mainfrom
fix/407-verify-reads-artifact
Sep 2, 2026
Merged

jhfnetboy merged 3 commits into
mainfrom
fix/407-verify-reads-artifact

Conversation

@jhfnetboy

Copy link
Copy Markdown
Member

15_VerifyAPNTs asserted supply == MINT_AMOUNT, where MINT_AMOUNT came from whoever ran the check — and the natural way to answer "what should it be?" is to read the chain, at which point the assertion compares the chain to itself.

Same family as three other defects this week: an expected value taken from a source that cannot disagree with the thing under test.

Fix

14_RedeployAPNTs writes deployments/apnts-deploy-record.json — token, factory, chainId, mintTo, declared mintAmount — before the chain is consulted. 15_VerifyAPNTs reads its expectation from there. repo:dvt landed rotation-readback.json for the same reason.

A missing or empty record fails. "No record" and "record says zero" are different readings, and treating them alike is the absence-as-consent this repo has now been bitten by four times.

Five cases, all run against the live Sepolia deployment

case result
record matches the chain RESULT: OK
mintAmount hand-edited to 1 supply does not equal the DECLARED mint
record names a different token the deploy record is for a different token
record from a different chain the deploy record is from a different chain
record emptied no deploy record: cannot verify supply

The second is the issue's stated acceptance criterion. The other four guard the record itself — a record that could be about anything is not evidence about this deployment.

One thing deliberately not traded away

run() stays external view. vm.exists is non-view and using it would have quietly removed the property that makes this script unable to deploy, broadcast or repair anything. Absence is detected by reading and checking for content instead.

Closes #407.

https://claude.ai/code/session_016URk99bYV66BPtfQmP3gy2

… the operator (#407)

15_VerifyAPNTs asserted `supply == MINT_AMOUNT`, where MINT_AMOUNT came from
whoever ran the check — and the natural way to answer "what should it be?" is to
read the chain, at which point the assertion compares the chain to itself. Same
family as three other defects this week: an expected value taken from a source
that cannot disagree with the thing under test.

14_RedeployAPNTs now writes deployments/apnts-deploy-record.json — token, factory,
chainId, mintTo and the declared mintAmount — BEFORE the chain is consulted, and
15_VerifyAPNTs reads its expectation from there. repo:dvt landed
rotation-readback.json for the same reason.

A missing or empty record FAILS. "No record" and "record says zero" are different
readings, and treating them alike is the absence-as-consent this repo has been
bitten by four times.

Five cases, all run against the live Sepolia deployment, each with its own message:

  record matches the chain          RESULT: OK
  mintAmount hand-edited to 1       supply does not equal the DECLARED mint
  record names a different token    the deploy record is for a different token
  record from a different chain     the deploy record is from a different chain
  record emptied                    no deploy record: cannot verify supply

The second is the issue's acceptance criterion; the other four are guards on the
record itself, since a record that can be about anything is not evidence about
this deployment.

run() stays `external view`. vm.exists is non-view and would have quietly removed
the property that makes this script unable to deploy, broadcast or repair
anything; absence is detected by reading and checking for content instead.

Closes #407.

Claude-Session: https://claude.ai/code/session_016URk99bYV66BPtfQmP3gy2

@clestons clestons left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ REQUEST_CHANGES — AAStarCommunity/SuperPaymaster#417 @ 2e821b53fc73f79d690277aa0d5ccd7909d417dc

作者意图(我的读法):把 15_VerifyAPNTs 的期望值来源从「跑它的人」换成「部署时写下的声明」,因为一个由操作员提供、而操作员又只能去读链来回答的期望值,会让断言退化成自反的恒真式。

代码这一层做到了,而且我在链上把它核完了。 但操作手册那一层还在教被删掉的那套做法 —— 而那正是这个 PR 要根除的模式。


🔴 阻塞项 · deployments/deploy-record-apnts-3.5.0-sepolia.md:48 与 :55

「Reproduce:」那段调用的正是 15_VerifyAPNTs,而且仍然带着 MINT_AMOUNT:

EXPECT_CHAIN_ID=11155111 MINT_AMOUNT=2000000000000000000000000 \
APNTS=0x948C9d1B… FACTORY=0xc83EDcb8… \
forge script contracts/script/deployment/15_VerifyAPNTs.s.sol:VerifyAPNTs --rpc-url "$SEPOLIA_RPC_URL"

紧接着 :55:

MINT_AMOUNT must match what the deploy minted: the check is that supply equals exactly the declared amount…

本 PR 之后 15_VerifyAPNTs 一次都不读 MINT_AMOUNT。 实测:

$ grep -rn MINT_AMOUNT --include='*.sol' --include='*.md' --include='*.sh' . | grep -v /lib/
14_RedeployAPNTs.s.sol:92   uint256 mintAmount = vm.envOr("MINT_AMOUNT", uint256(0));   ← 部署侧,仍然需要
15_VerifyAPNTs.s.sol:104    // this. It used to read MINT_AMOUNT from the environment,   ← 只剩注释
deploy-record-apnts-3.5.0-sepolia.md:48   ← 仍在传给 15
deploy-record-apnts-3.5.0-sepolia.md:55   ← 仍在解释被删掉的那套机制

这份文档不是冻结的历史存档,是活的操作手册 —— 它自己这么说:

  • 标题:# aPNTs 3.5.0 — Sepolia, **current**
  • :104 的收尾步骤:「3. Only then flip config.sepolia.json, and re-run 15_VerifyAPNTs」

也就是说这份文档明确要求操作员将来再跑一次 15,而它给的命令带着一个已经失效的变量,
它给的理由描述的是一个已经不存在的机制。

对照更说明问题:15_VerifyAPNTs.s.sol 自己的文件头示例已经不带 MINT_AMOUNT 了:

 *   APNTS=0x... FACTORY=0x... forge script \
 *     contracts/script/deployment/15_VerifyAPNTs.s.sol:VerifyAPNTs \

代码侧的文档改了,操作员真正会去读的那份没改。

为什么这算「没达成意图」而不是「顺手可以再修的注记」:这个 PR 的论点是
「期望值不能来自跑验证的那个人」。而操作员看的就是这份手册,手册今天仍然在
教他把期望值作为参数传进去。机制换了、指令没换,等于把同一个坑留在了
唯一会有人照着做的地方 —— 这正是本仓库反复在抓的「措辞比机制强」。

处方(两处,都很小):

  1. :48 的命令删掉 MINT_AMOUNT=...(EXPECT_CHAIN_ID 请自己确认 15 还读不读它,我没查)。
  2. :55 那段改成:期望值来自 deployments/apnts-deploy-record.json 的 mintAmount,
    由 14_RedeployAPNTs 在部署时写下;记录缺失即失败。

怎么证明改对了:

grep -rn "MINT_AMOUNT" --include='*.md' deployments/ | grep -i verify   # 应为空
grep -rn "MINT_AMOUNT" --include='*.sol' contracts/script/deployment/   # 应只剩 14 的 :92 和 15 的注释

⚠️ 第一条要配一格正对照(例如 grep -rn "MINT_AMOUNT" deployments/ 在改之前必须非空),
否则「空集」和「grep 写错了」分不开。


🟡 [Low] 15_VerifyAPNTs.s.sol:117-120 —— 那条 require 的错误信息在「记录不存在」时永远不会出现

string memory rec = vm.readFile(recPath);
require(bytes(rec).length > 0, "no deploy record: cannot verify supply against a declared amount");

上面的注释说:

vm.exists is non-view … so absence is detected by reading and checking for content instead.

「vm.exists 非 view、readFile 是 view」这个前提是对的,我核了 forge-std/src/Vm.sol:
exists 在 :620 无修饰、readFile 在 :681 是 view,所以保住 run() external view 的取舍成立。

但「检查内容」不是缺席被发现的方式。 我写了个带正对照的探针实测:

场景 结果
正对照 文件存在 present len = 243,PASS —— 探针有效
文件缺失 vm.readFile 先 revert:vm.readFile: failed to open file "…/__does_not_exist__.json": No such file or directory (os error 2)

那条 require 在缺席这条路径上到不了,操作员看到的是 cheatcode 的原始报错,
而不是精心写的那句话。它仍然可达的只有「文件存在但为空」这一种。

行为本身是对的(fail-closed 保住了),坏的是注释描述了一个不存在的机制 ——
和阻塞项是同一个形状,只是小一号。改法二选一:

  • 注释改成「缺席由 vm.readFile 自身 revert 挡住;这条 require 覆盖的是存在但为空」;
  • 或者把两种情况都收成同一句话(例如先 require(bytes(rec).length > 0) 之外再无所谓,
    因为 revert 已经足够)。不要为了让 require 生效而改回 vm.exists —— 那会毁掉 view。

🟡 [Low] 14_RedeployAPNTs.s.sol:142 —— 写记录在 stopBroadcast 之后,模拟跑也会写

vm.stopBroadcast() 在 :110,vm.writeJson 在 :142,路径写死。两个后果:

  • 不带 --broadcast 的空跑会把已提交的记录覆盖成模拟产物的地址。 后果是响的
    (下次 15 会以「the deploy record is for a different token」失败)、且 git checkout 可恢复,
    所以是 Low —— 但这个文件现在是唯一的真值来源,值得写一句。
  • 同一时刻只能存在一条链的记录。 部署到第二条链会把第一条的记录冲掉。
    chainId 那道 require 让它失败得很响(安全方向),但第一条链此后无法再验证。
    建议路径带上 chainId(apnts-deploy-record-<chainid>.json),或至少在注释里说明这个限制。

🟡 [注记,非本 PR 引入] 15_VerifyAPNTs.s.sol:206 —— 结论行的措辞强于它实际检查过的东西

ALLOW_POST_DEPLOY_ACTIVITY=true 会跳过 :102 整个 fresh-state 块(包括本 PR 新加的记录比对),
但 :206 仍然无条件打印:

RESULT: OK - both owners are the Safe; every enumerable value is at its post-deploy default

这条在本 PR 之前就在,不算你引入的。但本 PR 恰好在编辑那个被跳过的块,
顺手把结论行改成随开关分叉(跳过时说清楚「fresh-state 检查已被 ALLOW_POST_DEPLOY_ACTIVITY 跳过」)
是最省事的时机。由 Codex 在本轮补扫中提出。


✅ 我在链上核完的部分(Sepolia,publicnode RPC)

提交的 deployments/apnts-deploy-record.json 与链上状态逐项相符:

字段 记录 链上实测
mintAmount 2000000000000000000000000 totalSupply() = 2000000000000000000000000 ✅
mintTo 0xb5600060… balanceOf(0xb5600060…) = 2000000000000000000000000 ✅
chainId 11155111 Sepolia ✅
aPNTs 0x948C9d1B… 合约存在且可调用 ✅

另核了本 PR 那几条 require 的对象:
SUPERPAYMASTER_ADDRESS() = 0x0 ✅、issuanceCap() = 0 ✅。

方案本身的关键一点是成立的:14 写进记录的 mintAmount 取自 :92 的
vm.envOr("MINT_AMOUNT", …),即部署时的声明,不是链上回读 —— 所以 15 的比较
确实有了一个「可能与链不一致」的独立来源。这就是这个 PR 的全部价值所在,它做到了。

其余已核:foundry.toml 在仓库根,vm.projectRoot() 因此解析到根,与提交文件所在的
deployments/ 一致;fs_permissions 含 { access = "read-write", path = "./" },读写都被允许;
两个脚本 forge build 通过;run() 仍是 view。


轮次

轮 结果
R1a DeepSeek(flash) 1/2 成立 —— 它提出「vm.readFile 在文件缺失时可能以不透明错误 revert,而不是走到 require」,方向完全正确,被上面的探针坐实。另一条(14 里的相对路径)不成立,那里用的就是 vm.projectRoot()。
R1b DeepSeek(安全) 0/2 —— 「提交的部署记录泄露 token 地址 / chainId / mintTo / mintAmount」:驳回。这些全部是 Sepolia 上的公开链上事实,我自己就是用公开 RPC 一条 cast call 读出来的;把它们从仓库里删掉不会让任何人少知道一个比特。
R2 Opus ⚠️ 未跑 —— 本会话的 Opus 子代理今天多次派发后转 idle 且不回传结果。不凑轮数。
R3 Codex v0.152.0 跑了。三条我都要求它证伪,三条全部 CONFIRM;F1 那条它自己去读了文档的自述状态(current / :104 的 re-run 指令)并指出 .sol 头部示例已经不带 MINT_AMOUNT,把「这是冻结的历史存档」这条退路堵死了。另补一条 MISSED(:206 结论行)。
R4 Opus ⚠️ 未跑(同上)。

如实标注:实跑 R1a + R1b + R3。裁决由我(执行器)基于链上实测与探针作出,不是 Opus 拍的板。

…ecks

pr-daemon's review of 2e821b5. The mechanism changed; the instructions did not.

1. [blocking] deployments/deploy-record-apnts-3.5.0-sepolia.md — the "Reproduce:"
   command still passed MINT_AMOUNT=2000000000000000000000000, and :55 still
   explained a variable no longer read. The file is not an archive: it says
   "current" and its step 3 asks for a future re-run. The only place an operator
   actually reads was still describing the deleted mechanism.
     before: grep -rn MINT_AMOUNT --include='*.md' deployments/  -> :48 and :55
     after:  same grep                                           -> empty
   EXPECT_CHAIN_ID is still read (15_VerifyAPNTs.s.sol:47), so it stays.

2. [low] 15_VerifyAPNTs.s.sol — the comment claimed absence was "detected by
   reading and checking for content". Measured, vm.readFile REVERTS first:
     record moved away -> vm.readFile: failed to open file "...11155111.json":
                          No such file or directory (os error 2), script failed
   Fail-closed either way; the comment described a mechanism that does not exist.
   vm.exists stays out — it is non-view and would cost run() its `view`.

3. [low] 14_RedeployAPNTs.s.sol — the record path was fixed, and written after
   stopBroadcast regardless of context. A dry run would overwrite a committed
   declaration with simulated addresses, and a second chain would evict the
   first. Now written only under ScriptBroadcast, to a path carrying the chain id;
   15 reads the same path. Committed record renamed accordingly.

4. [note, pre-existing] ALLOW_POST_DEPLOY_ACTIVITY=true skips the whole
   fresh-clone block — including this PR's record comparison — and the script
   still printed "every enumerable value is at its post-deploy default". The
   claim now shrinks with the checks:
     flag set                    -> RESULT: OK (REDUCED) ... were NOT checked
     flag set, record deleted    -> still OK (REDUCED), i.e. the block really is
                                    skipped, which is why the claim had to change
   And the doc's step 3 now says to use that flag, because wiring the token is
   exactly what makes the default assertions false.

Load-bearing check, confirmed against Sepolia: tampering mintAmount by +1 in the
record gives "supply does not equal the DECLARED mint: something else minted".

Claude-Session: https://claude.ai/code/session_016URk99bYV66BPtfQmP3gy2
clestons
clestons previously approved these changes Sep 2, 2026

@clestons clestons left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ APPROVE — AAStarCommunity/SuperPaymaster#417 @ 8c0bd18fb5ee5c8df5a1c6f307bafeea454fcb50(第 2 轮,增量复审)

四条全收。这一轮我没有停在读代码 —— 把 15_VerifyAPNTs 在真 Sepolia 上跑了五种配置。

端到端实测(forge script … --rpc-url <public Sepolia>,真链、真记录)

配置 结果
A 正常 RESULT: OK - both owners are the Safe; every enumerable value is at its post-deploy default
B 记录里 mintAmount +1 [Revert] supply does not equal the DECLARED mint: something else minted → script failed
C 记录文件移走 [Revert] vm.readFile: failed to open file "…/apnts-deploy-record.11155111.json": No such file or directory (os error 2) → script failed
D ALLOW_POST_DEPLOY_ACTIVITY=true RESULT: OK (REDUCED) … the fresh-clone defaults and the supply-vs-deploy-record comparison were NOT checked.
E ALLOW_POST_DEPLOY_ACTIVITY=true 且记录已移走 仍然 OK (REDUCED) ← 这一格才证明那块是真被跳过的,而不是「if 分支读起来是那个意思」
F 记录 chainId 改成 1 [Revert] the deploy record is from a different chain → script failed

B 是承重性证据:那条比较不是摆设,改一个 wei 就红。
E 是我最看重的一格:你自己也跑了同一格,理由也说对了 —— 跳过这件事必须自己可判定。

① 阻塞项(手册教被删的做法)—— 收了,带正对照

$ git grep -c MINT_AMOUNT 2e821b53 -- 'deployments/*.md'     # 正对照:改之前
2e821b53:deployments/deploy-record-apnts-3.5.0-sepolia.md:2
$ grep -rc MINT_AMOUNT --include='*.md' deployments/          # 改之后
(空)

两条读数一正一负,判据不是恒空。

EXPECT_CHAIN_ID 你保留是对的,我核了 15_VerifyAPNTs.s.sol:47:
vm.envOr("EXPECT_CHAIN_ID", uint256(10)),确实还在读,默认 OP 主网。

你顺手补的 :104 那处比我提的那两处更重要,而我没看到它。
步骤 1/2 恰恰设置了 SuperPaymaster 和 auto-approved spender —— 也就是 fresh-clone 断言检查的那批默认值,
所以步骤 3 那次 re-run 在接线之后必然失败。手册原文让操作员去跑一个注定红的命令,
而红的原因和「部署有问题」长得一模一样。现在写明要带 ALLOW_POST_DEPLOY_ACTIVITY=true
并说明那时它是 owner 检查不是部署检查 —— 上面的 D/E 两格正好是这段文字的实测背书。

② Low(注释描述了不存在的机制)—— 改注释,没动 vm.exists,对

新注释写的是我实测到的东西:

a missing record is caught by vm.readFile itself: measured, it REVERTS on absence … So the require below covers only the present-but-empty case; both are fail-closed

require 的文案也跟着改成了 deploy record is empty: …,和它实际覆盖的情况对上了。
run() 仍是 view(:38),vm.exists 没被引进来 —— 这个取舍保住了。

③ Low(记录路径)—— 两个问题都堵了,而且第二个比我建议的更彻底

路径带 chainId(apnts-deploy-record.<chainid>.json,15 在 :116 读同一形状),
已提交那份 git mv 成 .11155111.json。

写入门禁我单独探针验了语义,没有靠读文档:

$ forge script <一个只打印 vm.isContext 的临时脚本> --rpc-url <sepolia>   # 不带 --broadcast
  ScriptBroadcast = false
  ScriptDryRun    = true

所以 if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast)) 在空跑时确实不写盘。
你说得对,这比只加 chainId 更彻底 —— 同链空跑照样会覆盖,光靠文件名分不开。

④ 注记(结论行措辞强于检查)—— 收了,且你补的第二栏是关键

:217 / :220 现在随 checkedDefaults 分叉,REDUCED 那支逐项点名了没检查的东西。
上面 D/E 两栏就是它的实测。


🟡 两条注记,都不阻塞

① 这个 PR 现在夹带了 #416 的门脚本,而且是「拷贝」不是「基于」。

相对 main(38c531e0),#417 的 diff 里有 scripts/check-live-scripts-compile.sh +131 行,
内容与 #416 当前 head 逐字相同(我 diff <(git show ae5cc7b6:…) <(git show 8c0bd18f:…) 核过,无差异)。
但 git merge-base --is-ancestor ae5cc7b6 8c0bd18f 为假 —— 不是 stack,是拷贝。
而且 #417 不含 .github/workflows/security.yml(本 PR 改动的 workflow 文件数为 0)。

后果三条,都不严重但值得知道:

  • 单看 #417,一个 reviewer 要审 131 行与「部署记录」无关的内容;
  • 若 #417 先合,main 会拿到一个没有任何 CI job 调用的门脚本;
  • #416 今天已经推过三个 head。再推一个,这份拷贝就是陈旧的那一份。
    失效方式是响的(add/add 冲突),不会静默,所以只是注记。

合并前跑一条就能确认两边还没分叉:

diff <(git show <416-head>:scripts/check-live-scripts-compile.sh) \
     <(git show <417-head>:scripts/check-live-scripts-compile.sh) && echo IDENTICAL

② abis/abi.config.json 只有 buildTime 变了(2026-09-01T13:05:47Z → 13:44:40Z),
totalHash 和所有文件 hash 一字未动 —— 本地重新构建带出来的时间戳噪声,与本 PR 无关。
不影响任何东西,下次顺手剔掉即可。


轮次如实:本轮 R1 按 ABSOLUTE CONSTRAINT #5 豁免二(增量=修我上一轮的 findings)。
判断来自六格链上端到端实测(含带正对照的 grep、vm.isContext 语义探针、以及 E 那格「跳过是否可判定」的负对照)。

pr-daemon, non-blocking notes on 8c0bd18.

scripts/check-live-scripts-compile.sh belongs to #416 and arrived here as a COPY,
not a stack: byte-identical to #416's head at the time, but ae5cc7b is not an
ancestor of this branch, and this branch carries none of the security.yml change
that gives the script a caller. So #417 was 131 lines of unrelated review surface,
and merging it first would have put a gate script on main that no CI job invokes.
It came in through `git checkout <other-branch> -- <path>` while cleaning up a
probe, followed by `git add -A` — the same move that silently reverted a fix
earlier this week.

#416 has since moved on (87e9191), so this copy was already the stale one.

abis/abi.config.json: buildTime moved, totalHash and every per-file hash did not.
Local-rebuild timestamp noise, not an ABI change.

Claude-Session: https://claude.ai/code/session_016URk99bYV66BPtfQmP3gy2

@clestons clestons left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ APPROVE — AAStarCommunity/SuperPaymaster#417 @ 6caba7176da2ac3274eb62984963387e401c00f3(第 3 轮)

两条注记都收了,而且这一轮我不需要重跑那六格链上实测 —— 我先证明了不需要。

增量是纯「摘除」,本 PR 自己的四个文件一字未动

git diff 8c0bd18f..6caba717 -- <file> | wc -l:

文件 增量行数
contracts/script/deployment/14_RedeployAPNTs.s.sol 0
contracts/script/deployment/15_VerifyAPNTs.s.sol 0
deployments/apnts-deploy-record.11155111.json 0
deployments/deploy-record-apnts-3.5.0-sepolia.md 0
(正对照) scripts/check-live-scripts-compile.sh 137
(正对照) abis/abi.config.json 13

正对照非零,说明「0 行」不是量具坏了。所以上一轮在 8c0bd18f 上跑的六格
(正常 OK / mintAmount+1 revert / 记录移走 readFile revert / ALLOW=true 打 REDUCED /
ALLOW=true 且记录移走仍 REDUCED / chainId 改 1 revert)逐字适用于这个 head,
不重复。

注记 ① 门脚本已摘

$ git show 6caba717:scripts/check-live-scripts-compile.sh
fatal: path ... does not exist          # exit 128
$ git diff --stat $(git merge-base main 6caba717)..6caba717
4 files changed, 93 insertions(+), 14 deletions(-)

来路你自己说清楚了,那句比修复值钱:清理探针时的
git checkout <另一分支> -- <path> + git add -A,
和本周早些时候悄悄回滚掉你自己一个修复的是同一个动作。
这类污染的共同点是它不产生冲突、不报错、diff 看起来还挺合理。

注记 ② abis/abi.config.json 已回退

git diff main..6caba717 -- abis/abi.config.json → 0 行。
你先核了它只有 buildTime 一行变化、totalHash 与每文件 hash 未动再回滚,顺序是对的 ——
先确认它是噪声,再删;反过来就会把一个真实的 ABI 漂移当噪声抹掉。

CI

全绿,包含 Stage 2 — forge test + fuzz(11m8s pass) —— 上一轮我提示要等的那一格已经过了。
Stage 1 / Stage 3 / Gate self-test / Scan for secrets / preflight-report ×2 / test 均 pass。


轮次如实:R1 按 constraint #5 豁免二(增量=摘除我上一轮点出的两处,无新逻辑)。
判断来自逐文件 0 行增量(带正对照)+ 上一轮六格链上实测的继承 + CI 读数。
R2/R4 的 Opus 未跑(本会话 Opus 子代理持续转 idle 不回传)。

@jhfnetboy
jhfnetboy merged commit e6ec303 into main Sep 2, 2026
12 checks passed
@jhfnetboy
jhfnetboy deleted the fix/407-verify-reads-artifact branch September 2, 2026 11:03
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

15_VerifyAPNTs takes its expected supply from the operator, which makes the check an identity

2 participants