From 31263e04721f3b633ec66930fd9841aa0746048b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?=
Date: Thu, 4 Jun 2026 12:24:39 +0000
Subject: [PATCH 1/7] fix(ecdsa/deploy): allowlist redeploy-safe networks for
EcdsaDkgValidator
Invert mainnet-only denylist to explicit allowlist (hardhat, development,
sepolia). Future production-like networks added downstream now default to
the safe behavior (preserve existing deployment) instead of silently
overwriting deployments//EcdsaDkgValidator.json.
---
solidity/ecdsa/deploy/02_deploy_dkg_validator.ts | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/solidity/ecdsa/deploy/02_deploy_dkg_validator.ts b/solidity/ecdsa/deploy/02_deploy_dkg_validator.ts
index fd0bd97546..c24b50b70c 100644
--- a/solidity/ecdsa/deploy/02_deploy_dkg_validator.ts
+++ b/solidity/ecdsa/deploy/02_deploy_dkg_validator.ts
@@ -15,11 +15,15 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const EcdsaSortitionPool = await deployments.get("EcdsaSortitionPool")
- // Non-mainnet: skipIfAlreadyDeployed false so hardhat-deploy can redeploy when bytecode
- // changes (e.g. groupSize 100 → 3). Mainnet: true so bytecode/artifact drift cannot
- // silently overwrite deployments/mainnet/EcdsaDkgValidator.json while WalletRegistry
- // still points at the old on-chain validator (THRESHOLD_FORCE_DKG_COMPILE only forces compile).
- const skipIfAlreadyDeployed = hre.network.name === "mainnet"
+ // Allowlist of networks where bytecode redeploy on artifact change is safe
+ // (e.g. groupSize 100 → 3 during local/testnet iteration). Any other network
+ // (mainnet and any future production-like alias) keeps the existing
+ // deployment record so bytecode/artifact drift cannot silently overwrite
+ // deployments//EcdsaDkgValidator.json while WalletRegistry still
+ // points at the old on-chain validator. THRESHOLD_FORCE_DKG_COMPILE only
+ // forces compile, not redeploy.
+ const redeploySafeNetworks = new Set(["hardhat", "development", "sepolia"])
+ const skipIfAlreadyDeployed = !redeploySafeNetworks.has(hre.network.name)
const EcdsaDkgValidator = await deployments.deploy("EcdsaDkgValidator", {
from: deployer,
From a9eaff9b5afb71043aa4673f7a6d8515e37bf214 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?=
Date: Thu, 4 Jun 2026 12:24:58 +0000
Subject: [PATCH 2/7] docs(ecdsa/deploy): document vendored
random-beacon-export format policy
Add README explaining that 05_approve_random_beacon_in_token_staking.js
intentionally diverges from its tsc-compiled siblings to carry an
ifaceHasFunction precheck for Threshold TokenStaking. A blind regeneration
from upstream would silently drop the precheck and reintroduce a hard
failure on networks without approveApplication.
---
.../random-beacon-export/deploy/README.md | 34 +++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 solidity/ecdsa/external/random-beacon-export/deploy/README.md
diff --git a/solidity/ecdsa/external/random-beacon-export/deploy/README.md b/solidity/ecdsa/external/random-beacon-export/deploy/README.md
new file mode 100644
index 0000000000..a28244da57
--- /dev/null
+++ b/solidity/ecdsa/external/random-beacon-export/deploy/README.md
@@ -0,0 +1,34 @@
+# `random-beacon-export/deploy`
+
+Vendored copies of `@keep-network/random-beacon`'s `export/deploy` scripts.
+Resolved by `hardhat.config.ts:resolveRandomBeaconExport()` as the second
+preference after `../random-beacon/export/` (gitignored upstream) and before
+the published `node_modules/@keep-network/random-beacon/export`.
+
+## Format
+
+These files intentionally mix two formats:
+
+- **`01..04, 06..09_*.js`**: `tsc`-compiled ES5 output from the upstream
+ package's TypeScript sources (`__awaiter` / `__generator` runtime helpers,
+ `var` declarations). Treat as build artifacts; do not hand-edit.
+- **`05_approve_random_beacon_in_token_staking.js`**: hand-written modern
+ async/await. Adds an `ifaceHasFunction("approveApplication")` precheck plus
+ an exception backstop so the script is idempotent against the Threshold
+ `TokenStaking` ABI (which does not expose `approveApplication`).
+ **Do not regenerate from upstream without preserving this precheck** —
+ blind regeneration will reintroduce a hard failure on networks running the
+ Threshold staking contract.
+
+## Regeneration policy
+
+When syncing from upstream:
+
+1. Regenerate `01..04, 06..09_*.js` from `@keep-network/random-beacon`'s
+ `export/deploy` source via its `tsc` build.
+2. **Skip `05_*.js`** during regeneration, or re-apply the
+ `ifaceHasFunction` precheck and the try/catch around `execute(...)` after
+ regenerating.
+3. Verify by running deploys against both a network that exposes
+ `approveApplication` (legacy Keep TokenStaking) and one that does not
+ (Threshold TokenStaking).
From eb956f9b7dc0f6dcd3f7dd2b0ca66bc4b8a73068 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?=
Date: Thu, 4 Jun 2026 12:25:48 +0000
Subject: [PATCH 3/7] fix(ecdsa/hardhat): restore HardhatUserConfig type
annotation
The conditional etherscan spread inside the object literal forced removal
of the HardhatUserConfig annotation. Assign etherscan via a post-declaration
if-block instead so the annotation (and type-checking) is preserved without
relying on TS 4.9 'satisfies' (this package is on TS 4.5).
---
solidity/ecdsa/hardhat.config.ts | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/solidity/ecdsa/hardhat.config.ts b/solidity/ecdsa/hardhat.config.ts
index cc82572c97..deff48b233 100644
--- a/solidity/ecdsa/hardhat.config.ts
+++ b/solidity/ecdsa/hardhat.config.ts
@@ -20,6 +20,8 @@ import "./tasks"
import { task } from "hardhat/config"
import { TASK_TEST } from "hardhat/builtin-tasks/task-names"
+import type { HardhatUserConfig } from "hardhat/config"
+
const TASK_CHECK_ACCOUNTS_COUNT = "check-accounts-count"
const hardhatVerifyEnabled = process.env.DISABLE_HARDHAT_VERIFY !== "true"
@@ -79,7 +81,7 @@ export const testConfig = {
operatorsCount: 100,
}
-const config = {
+const config: HardhatUserConfig = {
solidity: {
compilers: [
{
@@ -171,13 +173,6 @@ const config = {
username: "thesis",
project: "",
},
- ...(hardhatVerifyEnabled
- ? {
- etherscan: {
- apiKey: process.env.ETHERSCAN_API_KEY,
- },
- }
- : {}),
namedAccounts: {
deployer: {
default: 1, // take the second account
@@ -291,4 +286,14 @@ task(TASK_CHECK_ACCOUNTS_COUNT, "Checks accounts count").setAction(async () => {
}
})
+if (hardhatVerifyEnabled) {
+ // Assigned post-declaration so the HardhatUserConfig type annotation above
+ // remains intact (a conditional spread inside the literal breaks inference).
+ ;(config as HardhatUserConfig & {
+ etherscan?: { apiKey?: string }
+ }).etherscan = {
+ apiKey: process.env.ETHERSCAN_API_KEY,
+ }
+}
+
export default config
From 7740fb4aaca003c9a66b5a9328eabd5a5b67c1dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?=
Date: Thu, 4 Jun 2026 12:26:16 +0000
Subject: [PATCH 4/7] docs(ecdsa/hardhat): note sepolia named-account role
collapse
Sepolia maps deployer/governance/chaosnetOwner/esdm all to account index 0
because the testnet deploy uses a single key from ACCOUNTS_PRIVATE_KEYS.
Add a header comment so readers don't expect role-separation branches
(e.g. initialize-wallet-owner.ts's owner-vs-governance fork) to fire on
Sepolia.
---
solidity/ecdsa/hardhat.config.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/solidity/ecdsa/hardhat.config.ts b/solidity/ecdsa/hardhat.config.ts
index deff48b233..f8de665347 100644
--- a/solidity/ecdsa/hardhat.config.ts
+++ b/solidity/ecdsa/hardhat.config.ts
@@ -173,6 +173,12 @@ const config: HardhatUserConfig = {
username: "thesis",
project: "",
},
+ // Sepolia: all four roles collapse to account index 0 (the single key in
+ // ACCOUNTS_PRIVATE_KEYS). Single-key operation is intentional for our
+ // testnet deploy flow; downstream branches that distinguish deployer vs.
+ // governance vs. esdm (e.g. tasks/initialize-wallet-owner.ts's
+ // owner-vs-governance fork, Ownable.transferOwnership flows) are inactive
+ // on Sepolia by design.
namedAccounts: {
deployer: {
default: 1, // take the second account
From d6d03c4bfcf1b3a6befa850ed41e23fc1393b64f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?=
Date: Thu, 4 Jun 2026 12:26:34 +0000
Subject: [PATCH 5/7] docs(ecdsa/tasks): explain TOCTOU recheck of
WR.governance()
The second WalletRegistry.governance() read immediately before execute() is
a deliberate recheck against concurrent transferGovernance on shared
networks (sepolia/mainnet); annotate so it isn't mistaken for dead code.
---
solidity/ecdsa/tasks/initialize-wallet-owner.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/solidity/ecdsa/tasks/initialize-wallet-owner.ts b/solidity/ecdsa/tasks/initialize-wallet-owner.ts
index 830eac02a2..3071fb2733 100644
--- a/solidity/ecdsa/tasks/initialize-wallet-owner.ts
+++ b/solidity/ecdsa/tasks/initialize-wallet-owner.ts
@@ -53,6 +53,9 @@ async function initializeWalletOwner(
return
}
+ // TOCTOU recheck: governance can be transferred between the early read
+ // above and this execute() on shared networks. Re-read immediately before
+ // the tx so a concurrent transferGovernance doesn't slip past the gate.
const wrGovernanceNow = await read("WalletRegistry", {}, "governance")
if (!helpers.address.equal(wrg.address, wrGovernanceNow)) {
throw new Error(
From f97dc40cc7de9124c3323675fca1892f34524195 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?=
Date: Thu, 4 Jun 2026 12:27:15 +0000
Subject: [PATCH 6/7] fix(ecdsa/deploy): apply tenderly verify-or-continue
helper everywhere
Add verifyOnTenderlyOrContinue mirroring verifyOnEtherscanOrContinue and
wrap tenderly.verify in all four deploy scripts (01, 02, 03, 09). Before
this, only 03 swallowed Tenderly errors; a Tenderly outage during deploy
would fail 01/02/09 while 03 kept going. Now all four behave the same.
---
.../deploy/01_deploy_ecdsa_sortition_pool.ts | 11 ++++++----
.../ecdsa/deploy/02_deploy_dkg_validator.ts | 11 ++++++----
.../ecdsa/deploy/03_deploy_wallet_registry.ts | 9 ++++-----
.../09_deploy_wallet_registry_governance.ts | 11 ++++++----
solidity/ecdsa/deploy/tenderlyVerification.ts | 20 +++++++++++++++++++
5 files changed, 45 insertions(+), 17 deletions(-)
create mode 100644 solidity/ecdsa/deploy/tenderlyVerification.ts
diff --git a/solidity/ecdsa/deploy/01_deploy_ecdsa_sortition_pool.ts b/solidity/ecdsa/deploy/01_deploy_ecdsa_sortition_pool.ts
index 8003895569..800bab4aa1 100644
--- a/solidity/ecdsa/deploy/01_deploy_ecdsa_sortition_pool.ts
+++ b/solidity/ecdsa/deploy/01_deploy_ecdsa_sortition_pool.ts
@@ -1,4 +1,5 @@
import verifyOnEtherscanOrContinue from "./etherscanVerification"
+import verifyOnTenderlyOrContinue from "./tenderlyVerification"
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"
@@ -49,10 +50,12 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
}
if (hre.network.tags.tenderly) {
- await hre.tenderly.verify({
- name: "EcdsaSortitionPool",
- address: EcdsaSortitionPool.address,
- })
+ await verifyOnTenderlyOrContinue(hre, () =>
+ hre.tenderly.verify({
+ name: "EcdsaSortitionPool",
+ address: EcdsaSortitionPool.address,
+ })
+ )
}
return true
diff --git a/solidity/ecdsa/deploy/02_deploy_dkg_validator.ts b/solidity/ecdsa/deploy/02_deploy_dkg_validator.ts
index c24b50b70c..56ee25a748 100644
--- a/solidity/ecdsa/deploy/02_deploy_dkg_validator.ts
+++ b/solidity/ecdsa/deploy/02_deploy_dkg_validator.ts
@@ -1,4 +1,5 @@
import verifyOnEtherscanOrContinue from "./etherscanVerification"
+import verifyOnTenderlyOrContinue from "./tenderlyVerification"
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"
@@ -43,10 +44,12 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
}
if (hre.network.tags.tenderly) {
- await hre.tenderly.verify({
- name: "EcdsaDkgValidator",
- address: EcdsaDkgValidator.address,
- })
+ await verifyOnTenderlyOrContinue(hre, () =>
+ hre.tenderly.verify({
+ name: "EcdsaDkgValidator",
+ address: EcdsaDkgValidator.address,
+ })
+ )
}
return true
diff --git a/solidity/ecdsa/deploy/03_deploy_wallet_registry.ts b/solidity/ecdsa/deploy/03_deploy_wallet_registry.ts
index 4925c0c4f4..49d513f794 100644
--- a/solidity/ecdsa/deploy/03_deploy_wallet_registry.ts
+++ b/solidity/ecdsa/deploy/03_deploy_wallet_registry.ts
@@ -1,4 +1,5 @@
import verifyOnEtherscanOrContinue from "./etherscanVerification"
+import verifyOnTenderlyOrContinue from "./tenderlyVerification"
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"
@@ -78,14 +79,12 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
}
if (hre.network.tags.tenderly) {
- try {
- await hre.tenderly.verify({
+ await verifyOnTenderlyOrContinue(hre, () =>
+ hre.tenderly.verify({
name: "WalletRegistry",
address: walletRegistry.address,
})
- } catch (err) {
- hre.deployments.log(`Tenderly verification skipped: ${err}`)
- }
+ )
}
return true
diff --git a/solidity/ecdsa/deploy/09_deploy_wallet_registry_governance.ts b/solidity/ecdsa/deploy/09_deploy_wallet_registry_governance.ts
index 441377e215..09a9831e3a 100644
--- a/solidity/ecdsa/deploy/09_deploy_wallet_registry_governance.ts
+++ b/solidity/ecdsa/deploy/09_deploy_wallet_registry_governance.ts
@@ -1,4 +1,5 @@
import verifyOnEtherscanOrContinue from "./etherscanVerification"
+import verifyOnTenderlyOrContinue from "./tenderlyVerification"
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"
@@ -32,10 +33,12 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
}
if (hre.network.tags.tenderly) {
- await hre.tenderly.verify({
- name: "WalletRegistryGovernance",
- address: WalletRegistryGovernance.address,
- })
+ await verifyOnTenderlyOrContinue(hre, () =>
+ hre.tenderly.verify({
+ name: "WalletRegistryGovernance",
+ address: WalletRegistryGovernance.address,
+ })
+ )
}
}
diff --git a/solidity/ecdsa/deploy/tenderlyVerification.ts b/solidity/ecdsa/deploy/tenderlyVerification.ts
new file mode 100644
index 0000000000..bb17d51cab
--- /dev/null
+++ b/solidity/ecdsa/deploy/tenderlyVerification.ts
@@ -0,0 +1,20 @@
+import type { HardhatRuntimeEnvironment } from "hardhat/types"
+
+/**
+ * Runs Tenderly verification without failing the deploy when the Tenderly
+ * API errors (outages, missing project config, rate limits). Mirrors
+ * verifyOnEtherscanOrContinue so all post-deploy verification hooks behave
+ * the same way across scripts.
+ */
+export default async function verifyOnTenderlyOrContinue(
+ hre: HardhatRuntimeEnvironment,
+ verify: () => Promise
+): Promise {
+ try {
+ await verify()
+ } catch (err) {
+ hre.deployments.log(
+ `Tenderly verification skipped (deploy continues): ${err}`
+ )
+ }
+}
From 40e522f9149f1258f230cef8cc26e247cb4efa95 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?=
Date: Thu, 4 Jun 2026 13:02:16 +0000
Subject: [PATCH 7/7] fix(ecdsa/deploy): move README out of deploy/ dir
hardhat-deploy walks deploy/ and requires() every file; a Markdown
sibling there crashes deployments.fixture() in tests. Move the
regeneration-policy README one level up to
external/random-beacon-export/README.md (still discoverable, no longer
in the require path).
---
.../random-beacon-export/{deploy => }/README.md | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
rename solidity/ecdsa/external/random-beacon-export/{deploy => }/README.md (74%)
diff --git a/solidity/ecdsa/external/random-beacon-export/deploy/README.md b/solidity/ecdsa/external/random-beacon-export/README.md
similarity index 74%
rename from solidity/ecdsa/external/random-beacon-export/deploy/README.md
rename to solidity/ecdsa/external/random-beacon-export/README.md
index a28244da57..b852e3c91e 100644
--- a/solidity/ecdsa/external/random-beacon-export/deploy/README.md
+++ b/solidity/ecdsa/external/random-beacon-export/README.md
@@ -1,9 +1,14 @@
-# `random-beacon-export/deploy`
+# `random-beacon-export`
-Vendored copies of `@keep-network/random-beacon`'s `export/deploy` scripts.
-Resolved by `hardhat.config.ts:resolveRandomBeaconExport()` as the second
-preference after `../random-beacon/export/` (gitignored upstream) and before
-the published `node_modules/@keep-network/random-beacon/export`.
+Vendored copies of `@keep-network/random-beacon`'s `export/deploy` scripts,
+under `./deploy/`. Resolved by `hardhat.config.ts:resolveRandomBeaconExport()`
+as the second preference after `../random-beacon/export/` (gitignored
+upstream) and before the published
+`node_modules/@keep-network/random-beacon/export`.
+
+This README lives one level above `deploy/` because hardhat-deploy walks
+that directory and tries to `require()` every file; a Markdown sibling
+there would crash deployment.
## Format