Repository navigation
Make published random-beacon/ecdsa deploy scripts runnable by consumers on ethers v5 and v6, and idempotent on replay #4319
Description
tbtc-v2 does not run the deploy scripts that @keep-network/random-beacon and @keep-network/ecdsa publish. It runs hand-maintained copies of 13 of them, installed over node_modules by a postinstall hook (solidity/scripts/apply-solidity-contracts-export-deploy-patch.sh, copies in solidity/deploy-patches/). Every copy is a modern-JS rewrite, not our build output, pinned by header comment to random-beacon@2.1.0-dev.18 / ecdsa@2.1.0-dev.19. Anything we change in those 13 scripts is invisible to tbtc-v2 until someone edits the copy by hand, and our main already differs from the copies (for example random-beacon/05 gained its own approved-status check).
This issue is to make the published scripts runnable as-is by their consumers, so the overrides can be deleted.
Which scripts
| Package | Overridden | Untouched |
|---|---|---|
random-beacon (9 published) |
01, 02, 03, 04, 05_approve, 07_governance, 08_transfer_governance, 09_chaosnet | 06 |
ecdsa (18 published) |
03_deploy_wallet_registry, 07_approve_wallet_registry, 09_deploy_wallet_registry_governance, 10_transfer_governance, 13_authorize_in_random_beacon | the other 13 |
Four further overrides target @threshold-network/solidity-contracts (05, 07, 30, 32); that is a separate repo, tracked in threshold-network/solidity-contracts#184.
Two independent reasons for the overrides
1. Re-run idempotency (threshold-network/tbtc-v2#949, June 2026). tbtc-v2's testnet flow runs our scripts against a chain where our own deployment already ran, then copies the deployment JSON across and replays. The published scripts revert on the replay. The copies add "already done, skip" guards. Per the copies' own headers:
random-beacon/04:transferOwnershipreverts when deployer no longer ownsBeaconSortitionPool; skip when the pool is already owned by this RandomBeacon.random-beacon/05,ecdsa/07:approveApplicationreverts with "Can't approve application" when already APPROVED; also tolerateapplicationInforead failures and TokenStaking variants without the function.random-beacon/08,ecdsa/10:transferGovernancemust be called by the current governance, not deployer, once governance has moved; skip when it has.ecdsa/13:setRequesterAuthorizationthrough a RandomBeaconGovernance that is not the active governance fails "Caller is not the governance"; skip unless the deployed RBG is the on-chain governance.ecdsa/03:helpers.upgrades.deployProxythrows when WalletRegistry is already indeployments/; skip the proxy deploy.ecdsa/09: redeploying WalletRegistryGovernance orphans the live one.
Some of this has since landed on main in a different shape (e.g. random-beacon/05 on main checks applicationInfo before approving), which is exactly the divergence problem: the fix here never reaches tbtc-v2.
2. ethers runtime coupling (threshold-network/tbtc-v2#1067). The scripts execute inside the consumer's Hardhat, so hre.ethers and hre.helpers are whatever the consumer has installed. tbtc-v2 is moving to ethers v6 / @nomicfoundation/hardhat-ethers v3 / @keep-network/hardhat-helpers 0.7 while we stay on v5. Constructs in our scripts that only exist in v5, all verified against main:
| Construct | Where | Under ethers v6 |
|---|---|---|
hre.ethers.provider.waitForTransaction(hash, 2, 300000) (behind network.tags.etherscan) |
random-beacon/01, 02, 03, 04, 07, 09 |
HardhatEthersProvider.waitForTransaction throws NotImplementedError |
new ethers.utils.Interface(abi) |
random-beacon/05, ecdsa/07, ecdsa/17 |
ethers.utils is undefined |
.address on an ethers Contract (from deployProxy / getContract*) |
ecdsa/03; ecdsa/16 (8 sites), ecdsa/17 (34 sites) |
v6 contracts expose target / getAddress(); .address is undefined |
ethers.constants.*, ethers.utils.getAddress, .deployed() |
ecdsa/16, ecdsa/17 |
removed or renamed in v6 |
ecdsa/15–17 are on main but not yet published (18 published, 22 on main). They are env-gated, but the next ecdsa release ships them into every consumer's external.contracts[].deploy.
What "fixed" means
The published export/deploy/*.js must run unchanged under both:
- ethers 5 +
@nomiclabs/hardhat-ethers2 +hardhat-helpers0.6 (us, today), and - ethers 6 +
@nomicfoundation/hardhat-ethers3 +hardhat-helpers0.7 (tbtc-v2 after Reject duplicate group member candidate tickets #1067).
That rules out migrating the scripts to v6; it means keeping them off the parts of the ethers surface that differ:
- Waits. Replace
provider.waitForTransaction(hash, 2, 300000)withwaitConfirmationson thedeploy()call (supported by hardhat-deploy 0.11.x and 1.0.4, which wait using hardhat-deploy's own bundled ethers, independent ofhre.ethers), or withprovider.getTransaction(hash)thentx.wait(2)(valid in both versions; v6 additionally accepts a timeout). - ABI encoding and existence checks. Use
deployments.read/deployments.execute, and check the ABI JSON directly (abi.some(f => f.type === "function" && f.name === "approveApplication")) instead ofethers.utils.Interface. - Addresses. Take addresses from hardhat-deploy
Deploymentobjects ((await deployments.get(name)).address), never from ethersContractobjects. FordeployProxyresults, use the returned deployment record rather than the contract. - Constants and utilities. Literal zero address; address comparison via a helper that exists in both
hardhat-helpers0.6 and 0.7, or plain lower-casing. - Idempotency guards. Absorb the "already done" checks from reason 1 into the scripts themselves, using
deployments.readfor the on-chain state, so a replay is a no-op rather than a revert. The copies in tbtc-v2solidity/deploy-patches/are the spec for which states to handle.
Verification
Add a CI job that, after prepack, executes the published export/deploy from a minimal consumer Hardhat project on ethers 6 / hardhat-ethers 3 / hardhat-helpers 0.7 (the tbtc-v2 #1067 stack), and once on our own v5 stack, against a fresh Hardhat network. Then run it a second time against the same network to prove the replay is a no-op. That is the test tbtc-v2 currently performs for us, after publication, by overwriting our files.
Retirement
- Both packages: waits, ABI handling, addresses, constants made runtime-agnostic across all scripts on
main, includingecdsa/15–17. - Idempotency guards from the tbtc-v2 copies absorbed upstream.
- Consumer smoke test in CI, v5 and v6, single run and replay.
- Publish
random-beaconandecdsa; tbtc-v2 bumps its pins (they are dist-tag pinned since tbtc-v2#1058). - tbtc-v2 deletes the 13 keep-core entries from
deploy-patches/and the apply script, and verifies with itsUSE_EXTERNAL_DEPLOY=trueparity capture. - Sibling issue filed: Make published deploy scripts runnable by consumers on ethers v5 and v6, and idempotent on replay solidity-contracts#184 covers its 4 overridden scripts.
Related
#4295 (hardhat-deploy v2 port; this is a prerequisite, since v2 changes the script format again and the same consumers execute it), #4209, #4305, threshold-network/tbtc-v2#1067, threshold-network/tbtc-v2#1128, threshold-network/tbtc-v2#949.
Activity
linear commentedon Sep 8, 2026
ENG-759
fix(solidity): make published deploy scripts compatible with ethers v…