Skip to content

Make published random-beacon/ecdsa deploy scripts runnable by consumers on ethers v5 and v6, and idempotent on replay #4319

Open

Description

@mswilkison

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: transferOwnership reverts when deployer no longer owns BeaconSortitionPool; skip when the pool is already owned by this RandomBeacon.
  • random-beacon/05, ecdsa/07: approveApplication reverts with "Can't approve application" when already APPROVED; also tolerate applicationInfo read failures and TokenStaking variants without the function.
  • random-beacon/08, ecdsa/10: transferGovernance must be called by the current governance, not deployer, once governance has moved; skip when it has.
  • ecdsa/13: setRequesterAuthorization through 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.deployProxy throws when WalletRegistry is already in deployments/; 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:

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) with waitConfirmations on the deploy() call (supported by hardhat-deploy 0.11.x and 1.0.4, which wait using hardhat-deploy's own bundled ethers, independent of hre.ethers), or with provider.getTransaction(hash) then tx.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 of ethers.utils.Interface.
  • Addresses. Take addresses from hardhat-deploy Deployment objects ((await deployments.get(name)).address), never from ethers Contract objects. For deployProxy results, 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-helpers 0.6 and 0.7, or plain lower-casing.
  • Idempotency guards. Absorb the "already done" checks from reason 1 into the scripts themselves, using deployments.read for the on-chain state, so a replay is a no-op rather than a revert. The copies in tbtc-v2 solidity/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, including ecdsa/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-beacon and ecdsa; 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 its USE_EXTERNAL_DEPLOY=true parity 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions