Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions solidity/ecdsa/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export/
external/npm
hardhat-dependency-compiler/
export.json
gasReporterOutput.json

# Contract artifacts
artifacts/
Expand Down
85 changes: 85 additions & 0 deletions solidity/ecdsa/deploy/00_log_external_deployments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"

/**
* Logs the externally-resolved deployments at the start of a deploy run.
*
* On Sepolia the hardhat-deploy `external.deployments.sepolia` array is
* intentionally empty so the npm `@threshold-network/solidity-contracts`
* artifacts (which carry transactionHashes that some RPC providers cannot
* resolve) are not used. The committed `deployments/sepolia/` snapshot is
* therefore the sole source of upstream contracts (TokenStaking, T,
* RandomBeacon, etc.) for that network.
*
* Surfacing the resolved set up-front turns a missing snapshot into a
* clear "expected upstream contract X not found" failure instead of an
* opaque "deployments.get returned undefined" deep inside a downstream
* script.
*/
const EXPECTED_EXTERNAL_ON_SEPOLIA = [
"T",
"TokenStaking",
"RandomBeacon",
"RandomBeaconGovernance",
"ReimbursementPool",
]

const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, network, getNamedAccounts } = hre
const { log } = deployments

const all = await deployments.all()
const names = Object.keys(all).sort()

log(
`Deploy starting on network "${network.name}" with ${names.length} pre-resolved deployments:`
)
names.forEach((name) => {
log(` - ${name} @ ${all[name].address}`)
})

if (network.name === "sepolia") {
const missing = EXPECTED_EXTERNAL_ON_SEPOLIA.filter(
(name) => !(name in all)
)
if (missing.length > 0) {
throw new Error(
`Sepolia deploy: expected upstream contracts missing from deployments/sepolia/: ${missing.join(
", "
)}. ` +
"external.deployments.sepolia is empty by design; the committed snapshot under " +
"deployments/sepolia/ is the sole source. Regenerate or copy the missing artifacts."
)
}
}

// On mainnet the deployer key must be distinct from governance / chaosnetOwner /
// esdm. Those three legitimately share the Threshold Council multisig address, but
// the deploy key must not be a Threshold Council signer — otherwise compromising
// a single key gives full governance. Sepolia legitimately collapses everything
// to one key (see hardhat.config.ts), so this guard runs only on mainnet.
if (network.name === "mainnet") {
const named = await getNamedAccounts()
const deployer = (named.deployer || "").toLowerCase()
const collisions: string[] = ["governance", "chaosnetOwner", "esdm"].filter(
(role) => {
const addr = (named[role] || "").toLowerCase()
return Boolean(deployer && addr && deployer === addr)
}
)
if (collisions.length > 0) {
throw new Error(
`Mainnet deploy refused: deployer address (${named.deployer}) collides with ` +
`role(s) ${collisions.join(
", "
)}. Each of these must be a distinct address ` +
"to preserve the separation between the deploy key and governance."
)
}
}
}

export default func

func.tags = ["LogExternalDeployments"]
func.id = "log_external_deployments"
21 changes: 14 additions & 7 deletions solidity/ecdsa/deploy/00_resolve_reimbursement_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {

if (ReimbursementPool && helpers.address.isValid(ReimbursementPool.address)) {
log(`using existing ReimbursementPool at ${ReimbursementPool.address}`)
} else {
// In local/hardhat test runs this deployment may be intentionally absent.
if (hre.network.name === "hardhat" || hre.network.name === "development") {
log("ReimbursementPool not found on local network; skipping")
return
}
throw new Error("deployed ReimbursementPool contract not found")
return
}

if (hre.network.name === "hardhat" || hre.network.name === "development") {
throw new Error(
`ReimbursementPool not found on "${hre.network.name}". On local networks it is expected to come from the ` +
"random-beacon external deploys — set USE_EXTERNAL_DEPLOY=true (the package.json test/deploy:test scripts " +
"already do this) or pre-deploy ReimbursementPool yourself before running this script."
)
}
throw new Error(
`ReimbursementPool not found on "${hre.network.name}". For Sepolia, the committed ` +
"deployments/sepolia/ReimbursementPool.json is the source of truth. For mainnet, ./external/mainnet must " +
"contain ReimbursementPool.json."
)
}

export default func
Expand Down
21 changes: 14 additions & 7 deletions solidity/ecdsa/deploy/00_resolve_token_staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {

if (TokenStaking && helpers.address.isValid(TokenStaking.address)) {
log(`using existing TokenStaking at ${TokenStaking.address}`)
} else {
// In local/hardhat test runs this deployment may be intentionally absent.
if (hre.network.name === "hardhat" || hre.network.name === "development") {
log("TokenStaking not found on local network; skipping")
return
}
throw new Error("deployed TokenStaking contract not found")
return
}

if (hre.network.name === "hardhat" || hre.network.name === "development") {
throw new Error(
`TokenStaking not found on "${hre.network.name}". On local networks the contract is expected to come from ` +
"@threshold-network/solidity-contracts external deploys — set USE_EXTERNAL_DEPLOY=true (the package.json test/deploy:test " +
"scripts already do this) or pre-deploy TokenStaking yourself before running this script."
)
}
throw new Error(
`TokenStaking not found on "${hre.network.name}". For Sepolia, the committed deployments/sepolia/TokenStaking.json ` +
"is the source of truth (external.deployments.sepolia is empty by design). For mainnet, ./external/mainnet must " +
"contain TokenStaking.json."
)
}

export default func
Expand Down
16 changes: 16 additions & 0 deletions solidity/ecdsa/deploy/02_deploy_dkg_validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
// still points at the old on-chain validator (THRESHOLD_FORCE_DKG_COMPILE only forces compile).
const skipIfAlreadyDeployed = hre.network.name === "mainnet"

// If a prior validator deployment exists and we're about to redeploy it (non-mainnet),
// log a loud warning. The WalletRegistry stores the validator address in its constructor
// args, so a new validator deploy that does not also re-deploy / re-initialize the
// WalletRegistry will leave WR pointing at the OLD on-chain validator.
if (!skipIfAlreadyDeployed) {
const existing = await deployments.getOrNull("EcdsaDkgValidator")
if (existing) {
deployments.log(
`WARN: redeploying EcdsaDkgValidator on "${hre.network.name}"; ` +
`previous address ${existing.address} will be replaced. ` +
"WalletRegistry still references the previous validator address until it is also redeployed " +
"(03_deploy_wallet_registry.ts must run, or the WR proxy must be re-pointed manually)."
)
}
}

const EcdsaDkgValidator = await deployments.deploy("EcdsaDkgValidator", {
from: deployer,
args: [EcdsaSortitionPool.address],
Expand Down
3 changes: 3 additions & 0 deletions solidity/ecdsa/deploy/03_deploy_wallet_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
address: walletRegistry.address,
})
} catch (err) {
if (hre.network.name === "mainnet") {
throw err
}
hre.deployments.log(`Tenderly verification skipped: ${err}`)
}
}
Expand Down
23 changes: 6 additions & 17 deletions solidity/ecdsa/deploy/07_approve_wallet_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,12 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
return
}

try {
await execute(
"TokenStaking",
{ from: deployer, log: true, waitConfirmations: 1 },
"approveApplication",
WalletRegistry.address
)
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e)
if (msg.includes("No method named") && msg.includes("approveApplication")) {
hre.deployments.log(
"TokenStaking has no approveApplication callable on this network; skipping WalletRegistry approval"
)
return
}
throw e
}
await execute(
"TokenStaking",
{ from: deployer, log: true, waitConfirmations: 1 },
"approveApplication",
WalletRegistry.address
)
}

export default func
Expand Down
19 changes: 16 additions & 3 deletions solidity/ecdsa/deploy/etherscanVerification.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import type { HardhatRuntimeEnvironment } from "hardhat/types"

/**
* Runs Etherscan verification without failing the deploy when the explorer API
* errors (rate limits, bytecode mismatch, missing keys). Use for all deploy
* scripts so behavior matches across the stack.
* Runs Etherscan verification, tolerating explorer-side flakiness on
* non-mainnet networks while surfacing real failures on mainnet.
*
* On non-mainnet networks (Sepolia, etc.) the deploy continues if verify
* throws — typical reasons are rate limits, missing API keys, or bytecode
* mismatch on a testnet redeploy, none of which should block a fresh
* environment.
*
* On mainnet a verify failure can indicate genuine source/artifact
* divergence, so the error is rethrown to fail loud. Operators who
* intentionally want to skip verification on mainnet should gate the
* call at the deploy script level (e.g. DISABLE_HARDHAT_VERIFY=true)
* rather than silently swallowing here.
*/
export default async function verifyOnEtherscanOrContinue(
hre: HardhatRuntimeEnvironment,
Expand All @@ -12,6 +22,9 @@ export default async function verifyOnEtherscanOrContinue(
try {
await verify()
} catch (err) {
if (hre.network.name === "mainnet") {
throw err
}
hre.deployments.log(
`Etherscan verification skipped (deploy continues): ${err}`
)
Expand Down
43 changes: 43 additions & 0 deletions solidity/ecdsa/external/random-beacon-export/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Bundled random-beacon deploy scripts

This directory contains a committed copy of the `export/deploy/*.js` scripts that
the `@keep-network/random-beacon` package publishes to npm. The ecdsa package
needs these so its hardhat-deploy run can resolve random-beacon's deploy phase
when the local `../random-beacon/export/` directory is unavailable (it is
gitignored) and falling back to `node_modules/@keep-network/random-beacon/export`
would pull a stale published version.

The resolution order is defined in `solidity/ecdsa/hardhat.config.ts`
(`resolveRandomBeaconExport`): local sibling export first, this bundled copy
second, npm fallback last.

## Source

The scripts are the TypeScript-compiled output of `solidity/random-beacon/deploy/*.ts`,
produced by `yarn prepack` (i.e. `tsc -p tsconfig.export.json`) in the
`@keep-network/random-beacon` package.

## Regenerate

From the repo root:

```sh
cd solidity/random-beacon
yarn install
yarn prepack
cp export/deploy/*.js ../ecdsa/external/random-beacon-export/deploy/
```

Then verify `git diff` matches the intended deploy-script change in the
sibling `solidity/random-beacon/deploy/*.ts` source — divergence between
the `.ts` source and the bundled `.js` is the failure mode this directory
guards against.

## Why we don't just `ts-node` the upstream

`hardhat-deploy` reads deploy scripts from the configured external paths as
plain CommonJS modules. The `external/*/deploy` directories are listed in
`hardhat.config.ts` and loaded via `require`, so they must be runnable JS.
The bundled `.js` here matches what `@keep-network/random-beacon` ships to
npm consumers, keeping the in-monorepo and published-consumer code paths
identical.
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,7 @@ async function func(hre) {
catch (e) {
hre.deployments.log("Could not read TokenStaking application status (continuing): ".concat(e));
}
try {
await execute("TokenStaking", { from: deployer, log: true, waitConfirmations: 1 }, "approveApplication", RandomBeacon.address);
}
catch (e) {
var msg = e instanceof Error ? e.message : String(e);
if (msg.includes("No method named") && msg.includes("approveApplication")) {
hre.deployments.log("TokenStaking has no approveApplication callable on this network; skipping");
return;
}
throw e;
}
await execute("TokenStaking", { from: deployer, log: true, waitConfirmations: 1 }, "approveApplication", RandomBeacon.address);
}
exports.default = func;
func.tags = ["RandomBeaconApprove"];
Expand Down
1 change: 0 additions & 1 deletion solidity/ecdsa/gasReporterOutput.json

This file was deleted.

22 changes: 15 additions & 7 deletions solidity/ecdsa/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// `@nomicfoundation/hardhat-verify` ^2.1.x is the Hardhat 2–compatible line; Hardhat 3 uses ^3.x (API v2).
// Set DISABLE_HARDHAT_VERIFY=true to omit the plugin and skip Etherscan steps in deploy scripts; default is on.
// Plugin is imported statically so HardhatUserConfig picks up the `etherscan` field type
// augmentation. Set DISABLE_HARDHAT_VERIFY=true to skip the Etherscan config and any verify
// calls in deploy scripts; the plugin's task registration is harmless when unused.
import fs from "fs"
import path from "path"

import "@nomicfoundation/hardhat-chai-matchers"
import "@nomicfoundation/hardhat-verify"
import "@keep-network/hardhat-helpers"
import "@keep-network/hardhat-local-networks-config"
import "@nomiclabs/hardhat-waffle"
Expand All @@ -20,13 +23,11 @@ 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"
if (hardhatVerifyEnabled) {
// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires,global-require
require("@nomicfoundation/hardhat-verify")
}

/**
* Random-beacon `export/` is gitignored in the random-beacon package, so CI never
Expand Down Expand Up @@ -79,7 +80,7 @@ export const testConfig = {
operatorsCount: 100,
}

const config = {
const config: HardhatUserConfig = {
solidity: {
compilers: [
{
Expand Down Expand Up @@ -178,10 +179,17 @@ const config = {
},
}
: {}),
// Sepolia uses a single key (account index 0 from ACCOUNTS_PRIVATE_KEYS) for every
// role because the testnet stack is operated from one funded deployer key — there is
// no separate governance multisig, Threshold Council, or chaosnet owner on Sepolia.
// This is testnet-only operational convenience and MUST NOT be replicated on mainnet,
// where the deployer key must be distinct from governance / chaosnetOwner / esdm
// (the latter three legitimately share the Threshold Council address). A deploy-time
// guard in deploy/00_log_external_deployments.ts fails the run on mainnet if the
// deployer collides with any other role.
namedAccounts: {
deployer: {
default: 1, // take the second account
// Use first account from ACCOUNTS_PRIVATE_KEYS (required for custom testnet deployers)
sepolia: 0,
mainnet: "0x716089154304f22a2F9c8d2f8C45815183BF3532",
},
Expand Down
2 changes: 1 addition & 1 deletion solidity/ecdsa/test/DKGValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe("EcdsaDkgValidator", () => {
before("load test fixture", async () => {
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;({ walletRegistry, sortitionPool, walletOwner } =
await walletRegistryFixture())
await walletRegistryFixture({ useAllowlist: true }))

validator = await helpers.contracts.getContract("EcdsaDkgValidator")

Expand Down
2 changes: 1 addition & 1 deletion solidity/ecdsa/test/WalletRegistry.Inactivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("WalletRegistry - Inactivity", () => {
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;({ walletRegistry, sortitionPool, randomBeacon, walletOwner, thirdParty } =
await walletRegistryFixture())
await walletRegistryFixture({ useAllowlist: true }))
;({ members, walletID } = await createNewWallet(
walletRegistry,
walletOwner.wallet,
Expand Down
2 changes: 1 addition & 1 deletion solidity/ecdsa/test/WalletRegistry.Parameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe("WalletRegistry - Parameters", async () => {
deployer,
governance,
thirdParty,
} = await walletRegistryFixture())
} = await walletRegistryFixture({ useAllowlist: true }))
})

describe("updateAuthorizationParameters", () => {
Expand Down
Loading
Loading