Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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 @@ -26,6 +26,22 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const redeploySafeNetworks = new Set(["hardhat", "development", "sepolia"])
const skipIfAlreadyDeployed = !redeploySafeNetworks.has(hre.network.name)

// 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
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
14 changes: 12 additions & 2 deletions solidity/ecdsa/deploy/tenderlyVerification.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
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
* Runs Tenderly verification, tolerating Tenderly-side flakiness on
* non-mainnet networks while surfacing real failures on mainnet.
*
* On non-mainnet networks the deploy continues if verify throws — typical
* reasons are outages, missing project config, or rate limits, 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. Mirrors
* verifyOnEtherscanOrContinue so all post-deploy verification hooks behave
* the same way across scripts.
*/
Expand All @@ -13,6 +20,9 @@ export default async function verifyOnTenderlyOrContinue(
try {
await verify()
} catch (err) {
if (hre.network.name === "mainnet") {
throw err
}
hre.deployments.log(
`Tenderly verification skipped (deploy continues): ${err}`
)
Expand Down
1 change: 1 addition & 0 deletions solidity/ecdsa/deployments/sepolia/.chainId
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
11155111
6 changes: 6 additions & 0 deletions solidity/ecdsa/deployments/sepolia/.migrations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"deploy_ecdsa_sortition_pool": 1777655041,
"deploy_ecdsa_dkg_validator": 1777655068,
"deploy_wallet_registry": 1777655124,
"deploy_allowlist": 1777655234
}
Loading
Loading