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 fd0bd97546..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" @@ -15,11 +16,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, @@ -39,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}` + ) + } +} diff --git a/solidity/ecdsa/external/random-beacon-export/README.md b/solidity/ecdsa/external/random-beacon-export/README.md new file mode 100644 index 0000000000..b852e3c91e --- /dev/null +++ b/solidity/ecdsa/external/random-beacon-export/README.md @@ -0,0 +1,39 @@ +# `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 + +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). diff --git a/solidity/ecdsa/hardhat.config.ts b/solidity/ecdsa/hardhat.config.ts index cc82572c97..f8de665347 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,12 @@ const config = { username: "thesis", project: "", }, - ...(hardhatVerifyEnabled - ? { - etherscan: { - apiKey: process.env.ETHERSCAN_API_KEY, - }, - } - : {}), + // 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 @@ -291,4 +292,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 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(