Skip to content
11 changes: 7 additions & 4 deletions solidity/ecdsa/deploy/01_deploy_ecdsa_sortition_pool.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
Expand Down
25 changes: 16 additions & 9 deletions solidity/ecdsa/deploy/02_deploy_dkg_validator.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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/<network>/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,
Expand All @@ -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
Expand Down
9 changes: 4 additions & 5 deletions solidity/ecdsa/deploy/03_deploy_wallet_registry.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions solidity/ecdsa/deploy/09_deploy_wallet_registry_governance.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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,
})
)
}
}

Expand Down
20 changes: 20 additions & 0 deletions solidity/ecdsa/deploy/tenderlyVerification.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>
): Promise<void> {
try {
await verify()
} catch (err) {
hre.deployments.log(
`Tenderly verification skipped (deploy continues): ${err}`
)
}
}
39 changes: 39 additions & 0 deletions solidity/ecdsa/external/random-beacon-export/README.md
Original file line number Diff line number Diff line change
@@ -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).
27 changes: 19 additions & 8 deletions solidity/ecdsa/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -79,7 +81,7 @@ export const testConfig = {
operatorsCount: 100,
}

const config = {
const config: HardhatUserConfig = {
solidity: {
compilers: [
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions solidity/ecdsa/tasks/initialize-wallet-owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading