From 6a21610a29c5219cd47b15328111e568528938aa Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 1 Jun 2026 12:46:25 +0200 Subject: [PATCH 1/9] migrate contracts to Talso signer --- .../196_migrate_operators_to_talos_kms.js | 72 +++++++++++++++++++ contracts/utils/addresses.js | 1 + 2 files changed, 73 insertions(+) create mode 100644 contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js diff --git a/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js b/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js new file mode 100644 index 0000000000..e9ca44030a --- /dev/null +++ b/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js @@ -0,0 +1,72 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "196_migrate_operators_to_talos", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", // fill in after the proposal is submitted on-chain + }, + async () => { + // OUSD Vault (proxy "VaultProxy") + OETH Vault — IVault exposes both setters + const cVaultProxy = await ethers.getContract("VaultProxy"); + const cOUSDVault = await ethers.getContractAt( + "IVault", + cVaultProxy.address + ); + + const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cOETHVault = await ethers.getContractAt( + "IVault", + cOETHVaultProxy.address + ); + + // Cross-chain strategies (same contract code, two Create2 proxies) + const cCrossChainMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategy", + addresses.mainnet.CrossChainMasterStrategy + ); + const cCrossChainHyperEVMMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategy", + addresses.mainnet.CrossChainHyperEVMMasterStrategy + ); + + return { + name: "Migrate scheduled-action operator of the OUSD/OETH vaults and the Crosschain (Base + HyperEVM) strategies to the new signer, and unpause OUSD/OETH rebases.", + actions: [ + { + contract: cOUSDVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOETHVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cCrossChainMasterStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + { + contract: cCrossChainHyperEVMMasterStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOUSDVault, + signature: "unpauseRebase()", + args: [], + }, + { + contract: cOETHVault, + signature: "unpauseRebase()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index a05685af3a..f9bcf0c350 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -9,6 +9,7 @@ addresses.multichainStrategist = "0x4FF1b9D9ba8558F5EAfCec096318eA0d8b541971"; addresses.multichainBuybackOperator = "0xBB077E716A5f1F1B63ed5244eBFf5214E50fec8c"; addresses.talosRelayer = "0x0aBCDa6Fa7d500cf69B0eA5de9a607Cd9941221C"; +addresses.talosRelayer = "0x739212d5bAfE6AAC8Be49a60B7d003bD41DBf38b"; // new Talos signer addresses.votemarket = "0x8c2c5A295450DDFf4CB360cA73FCCC12243D14D9"; // CCTP contracts (uses same addresses on all chains) From 36aa134855ee969bb5bf7f71e19bfc17e7bf7d7e Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 2 Jun 2026 20:23:48 +0200 Subject: [PATCH 2/9] fix fork tests --- contracts/test/_fixture.js | 4 +++- contracts/utils/addresses.js | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 0943564e81..f41462c63c 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1632,8 +1632,10 @@ async function crossChainFixture() { addresses.CCTPTokenMessengerV2 ); + // The cross-chain operator is repointed during the Talos signer migration + // (deploy 196), so read it from the strategy instead of hardcoding a relayer. fixture.relayer = await impersonateAndFund( - addresses.mainnet.validatorRegistrator + await cCrossChainMasterStrategy.operator() ); await setERC20TokenBalance( diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index f9bcf0c350..d24ba89022 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -8,7 +8,6 @@ addresses.createX = "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed"; addresses.multichainStrategist = "0x4FF1b9D9ba8558F5EAfCec096318eA0d8b541971"; addresses.multichainBuybackOperator = "0xBB077E716A5f1F1B63ed5244eBFf5214E50fec8c"; -addresses.talosRelayer = "0x0aBCDa6Fa7d500cf69B0eA5de9a607Cd9941221C"; addresses.talosRelayer = "0x739212d5bAfE6AAC8Be49a60B7d003bD41DBf38b"; // new Talos signer addresses.votemarket = "0x8c2c5A295450DDFf4CB360cA73FCCC12243D14D9"; From 2ae0aed3e3c8843ed8d197d4b28a318f0ef40292 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 3 Jun 2026 15:41:36 +0200 Subject: [PATCH 3/9] add the migration for the OGN rewards module --- .../197_migrate_xogn_module6_to_talos_kms.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js diff --git a/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js new file mode 100644 index 0000000000..4cc721e17b --- /dev/null +++ b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js @@ -0,0 +1,33 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGuardianGovernor } = require("../../utils/deploy"); + +// CollectXOGNRewardsModule6 is admined by the mainnet Guardian 5/8 Safe (its +// DEFAULT_ADMIN_ROLE holder), so the operator migration is executed by that Safe +// directly — a different signing entity than the GovernorSix -> Timelock used in +// deploy 196, hence a separate deploy file. +module.exports = deploymentWithGuardianGovernor( + { + deployName: "197_migrate_xogn_module6_to_talos", + forceDeploy: false, + }, + async () => { + const cCollectXOGNRewardsModule6 = await ethers.getContract( + "CollectXOGNRewardsModule6" + ); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + name: "Grant the OPERATOR_ROLE of CollectXOGNRewardsModule6 to the new Talos signer.", + actions: [ + { + contract: cCollectXOGNRewardsModule6, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + ], + }; + } +); From 87e519a4240b94970216b8055aea3cbe35863bdd Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 3 Jun 2026 23:57:26 +0200 Subject: [PATCH 4/9] refactor deployment --- .../197_migrate_xogn_module6_to_talos_kms.js | 5 +- ...migrate_strategist_modules_to_talos_kms.js | 39 +++++ contracts/utils/deploy.js | 160 ++++++++++++++++++ 3 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js diff --git a/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js index 4cc721e17b..0101ab5193 100644 --- a/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js +++ b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js @@ -1,13 +1,14 @@ const addresses = require("../../utils/addresses"); -const { deploymentWithGuardianGovernor } = require("../../utils/deploy"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); // CollectXOGNRewardsModule6 is admined by the mainnet Guardian 5/8 Safe (its // DEFAULT_ADMIN_ROLE holder), so the operator migration is executed by that Safe // directly — a different signing entity than the GovernorSix -> Timelock used in // deploy 196, hence a separate deploy file. -module.exports = deploymentWithGuardianGovernor( +module.exports = deploymentWithGnosisSafe( { deployName: "197_migrate_xogn_module6_to_talos", + safe: addresses.mainnet.Guardian, forceDeploy: false, }, async () => { diff --git a/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js b/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js new file mode 100644 index 0000000000..dc0b8df23d --- /dev/null +++ b/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js @@ -0,0 +1,39 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// All four safe modules are admined by the multichainStrategist 2/8 Safe (their +// DEFAULT_ADMIN_ROLE holder), so granting OPERATOR_ROLE to the new Talos signer +// is a plain Safe transaction batch — a different signing entity than the +// GovernorSix->Timelock (deploy 196) and the Guardian 5/8 Safe (deploy 197). +module.exports = deploymentWithGnosisSafe( + { + deployName: "198_migrate_strategist_modules_to_talos", + safe: addresses.multichainStrategist, + forceDeploy: false, + }, + async () => { + const moduleNames = [ + "ClaimStrategyRewardsSafeModule", + "AutoWithdrawalModule", + "MerklPoolBoosterBribesModule", + "CurvePoolBoosterBribesModule", + ]; + const modules = []; + for (const name of moduleNames) { + modules.push(await ethers.getContract(name)); + } + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + name: "Grant the OPERATOR_ROLE of all strategist safe modules to the new Talos signer.", + actions: modules.map((cModule) => ({ + contract: cModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + })), + }; + } +); diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index d03b7bf1ff..28c9699d6f 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -48,6 +48,8 @@ const { getStorageAt, } = require("@nomicfoundation/hardhat-network-helpers"); const { keccak256, defaultAbiCoder } = require("ethers/lib/utils.js"); +const fs = require("fs"); +const path = require("path"); // Wait for 3 blocks confirmation on Mainnet. let NUM_CONFIRMATIONS = isMainnet ? 3 : 0; @@ -993,6 +995,47 @@ async function buildGnosisSafeJson( }); } +// Inlined to avoid a circular import: deploy-l2.js already requires deploy.js, +// so its getNetworkName() can't be imported here. +function safeOpsNetworkName() { + if (isForkTest) return "hardhat"; + if (isFork) return "localhost"; + return process.env.NETWORK_NAME || "mainnet"; +} + +function safeValueToString(value) { + if (BigNumber.isBigNumber(value)) return value.toString(); + if (Array.isArray(value)) return JSON.stringify(value.map(safeValueToString)); + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + // address / bytes32 / bytes / string pass through unchanged + return String(value); +} + +// (contract, signature, args) -> { paramName: stringValue } for the Gnosis Safe +// Transaction Builder. Param names come from the ABI fragment. Tuple/struct +// params are unsupported (not used by the Safe migrations) and throw loudly. +function buildContractInputsValues(contract, signature, args) { + const inputs = contract.interface.getFunction(signature).inputs; + if (inputs.length !== args.length) { + throw new Error( + `${signature}: expected ${inputs.length} args, got ${args.length}` + ); + } + const out = {}; + inputs.forEach((input, i) => { + if (input.baseType === "tuple" || input.components) { + throw new Error( + `${signature}: tuple/struct param "${input.name}" is not supported` + ); + } + const name = input.name && input.name.length ? input.name : `arg${i}`; + out[name] = safeValueToString(args[i]); + }); + return out; +} + async function getProposalExecutionValue(governor, proposalId) { const actions = await governor.getActions(proposalId); const rawValues = @@ -1311,6 +1354,122 @@ function deploymentWithGuardianGovernor(opts, fn) { return main; } +/** + * Shortcut to create a deployment executed by a plain Gnosis Safe (NOT GovernorSix + * governance, NOT the Guardian-via-timelock path). The deploy fn returns a single + * list of actions ({ contract, signature, args, value }). The helper always builds + * the Gnosis Safe Transaction Builder JSON and writes it to + * deployments//operations/.json (mainnet -> committed + * artifact, fork -> gitignored). On fork it additionally executes the actions by + * impersonating the Safe, so fork tests pass and downstream deploys see the state. + * + * @param {Object} opts deployment options. `safe` is the executing Safe address. + * @param {Function} fn async (tools) => { name?, safe?, actions: [...] } + * @returns {Function} main object used by hardhat + */ +function deploymentWithGnosisSafe(opts, fn) { + const { deployName, dependencies, forceDeploy, onlyOnFork, forceSkip } = opts; + const optsSafe = opts.safe; + + const runDeployment = async (hre) => { + const assetAddresses = await getAssetAddresses(hre.deployments); + const proposal = await fn({ + assetAddresses, + deployWithConfirmation, + ethers, + getTxOpts, + withConfirmation, + }); + + if (!proposal?.actions?.length) { + log("No Safe proposal actions."); + return; + } + + const safeAddress = proposal.safe || optsSafe; + if (!safeAddress) { + throw new Error( + `deploymentWithGnosisSafe (${deployName}): no Safe address. ` + + `Set opts.safe or return { safe } from the deploy fn.` + ); + } + + const { actions } = proposal; + + // Build + write the Safe Transaction Builder JSON (every environment). + const safeJson = await buildGnosisSafeJson( + safeAddress, + actions.map((a) => a.contract.address), + actions.map((a) => constructContractMethod(a.contract, a.signature)), + actions.map((a) => + buildContractInputsValues(a.contract, a.signature, a.args) + ), + actions.map((a) => BigNumber.from(a.value ?? 0).toString()) + ); + const filePath = path.resolve( + __dirname, + `./../deployments/${safeOpsNetworkName()}/operations/${deployName}.json` + ); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(safeJson, null, 2)); + console.log(`Safe batch JSON written to ${filePath}`); + + if (isMainnet) { + // Mainnet: JSON only. The operator imports it into the Safe Transaction + // Builder for the Safe to execute. + console.log( + `Import ${deployName}.json into the Gnosis Safe (${safeAddress}) Transaction Builder to execute.` + ); + return; + } + + // Fork: execute the batch by impersonating the Safe. + const safeSigner = await impersonateAndFund(safeAddress); + console.log(`Impersonating Safe ${safeAddress} to execute actions on fork`); + for (const action of actions) { + const { contract, signature, args, value } = action; + const txOpts = { + ...(await getTxOpts()), + ...(value ? { value } : {}), + }; + log(`Sending Safe action ${signature} to ${contract.address}`); + await withConfirmation( + contract.connect(safeSigner)[signature](...args, txOpts) + ); + console.log(`... ${signature} completed`); + } + }; + + const main = async (hre) => { + console.log(`Running ${deployName} deployment...`); + if (!hre) { + hre = require("hardhat"); + } + await runDeployment(hre); + console.log(`${deployName} deploy done.`); + return true; + }; + + main.id = deployName; + main.dependencies = dependencies; + if (forceSkip) { + main.skip = () => true; + } else if (forceDeploy) { + main.skip = () => false; + } else { + main.skip = async () => { + if (isFork) { + const networkName = isForkTest ? "hardhat" : "localhost"; + const migrations = require(`./../deployments/${networkName}/.migrations.json`); + return Boolean(migrations[deployName]); + } else { + return onlyOnFork ? true : !isMainnet || isSmokeTest; + } + }; + } + return main; +} + function encodeSaltForCreateX(deployer, crosschainProtectionFlag, salt) { // Generate encoded salt (deployer address || crosschainProtectionFlag || bytes11(keccak256(rewardToken, gauge))) @@ -1471,6 +1630,7 @@ module.exports = { executeProposalOnFork, deploymentWithGovernanceProposal, deploymentWithGuardianGovernor, + deploymentWithGnosisSafe, constructContractMethod, buildGnosisSafeJson, From d42028feebf64619c87b574bc19e635a67d2c090 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 4 Jun 2026 00:04:21 +0200 Subject: [PATCH 5/9] add some comments --- .../deploy/mainnet/196_migrate_operators_to_talos_kms.js | 1 + .../deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js | 5 +---- .../mainnet/198_migrate_strategist_modules_to_talos_kms.js | 5 +---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js b/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js index e9ca44030a..b96300957c 100644 --- a/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js +++ b/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js @@ -1,6 +1,7 @@ const addresses = require("../../utils/addresses"); const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); +// the governor to execute this proposal is OGN governance module.exports = deploymentWithGovernanceProposal( { deployName: "196_migrate_operators_to_talos", diff --git a/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js index 0101ab5193..6567d1d144 100644 --- a/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js +++ b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js @@ -1,10 +1,7 @@ const addresses = require("../../utils/addresses"); const { deploymentWithGnosisSafe } = require("../../utils/deploy"); -// CollectXOGNRewardsModule6 is admined by the mainnet Guardian 5/8 Safe (its -// DEFAULT_ADMIN_ROLE holder), so the operator migration is executed by that Safe -// directly — a different signing entity than the GovernorSix -> Timelock used in -// deploy 196, hence a separate deploy file. +// the governor to execute this proposal is Gnosis 5/8 Multisig module.exports = deploymentWithGnosisSafe( { deployName: "197_migrate_xogn_module6_to_talos", diff --git a/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js b/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js index dc0b8df23d..c33de53bd3 100644 --- a/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js +++ b/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js @@ -1,10 +1,7 @@ const addresses = require("../../utils/addresses"); const { deploymentWithGnosisSafe } = require("../../utils/deploy"); -// All four safe modules are admined by the multichainStrategist 2/8 Safe (their -// DEFAULT_ADMIN_ROLE holder), so granting OPERATOR_ROLE to the new Talos signer -// is a plain Safe transaction batch — a different signing entity than the -// GovernorSix->Timelock (deploy 196) and the Guardian 5/8 Safe (deploy 197). +// the governor to execute this proposal is 2/8 Cross chain strategist module.exports = deploymentWithGnosisSafe( { deployName: "198_migrate_strategist_modules_to_talos", From 639548e6296610932cf312439cf84c67065cd35a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 4 Jun 2026 16:29:04 +0200 Subject: [PATCH 6/9] add base migration files --- ...051_migrate_base_operators_to_talos_kms.js | 45 +++++++++++++++++++ ..._migrate_base_merkl_module_to_talos_kms.js | 35 +++++++++++++++ contracts/utils/deploy.js | 33 +++++++++++--- 3 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js create mode 100644 contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js diff --git a/contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js b/contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js new file mode 100644 index 0000000000..6a83594b35 --- /dev/null +++ b/contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js @@ -0,0 +1,45 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); + +// Executed by base.governor 5/8 -> Base Timelock (these setters are onlyGovernor). +// Re-points the Base CrossChainRemoteStrategy operator and the OETHBaseVault +// operatorAddr to the new Talos signer, and unpauses OETHb rebases so Talos can +// rebase the vault directly (operator-gated) — replacing the PermissionedRebaseModule. +module.exports = deployOnBase( + { + deployName: "051_migrate_base_operators_to_talos", + }, + async () => { + const cCrossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + addresses.base.CrossChainRemoteStrategy + ); + + const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + const cOETHbVault = await ethers.getContractAt( + "IVault", + cOETHbVaultProxy.address + ); + + return { + name: "Migrate the Base CrossChainRemoteStrategy operator and OETHBaseVault operatorAddr to the new Talos signer, and unpause OETHb rebases.", + actions: [ + { + contract: cCrossChainRemoteStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOETHbVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOETHbVault, + signature: "unpauseRebase()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js b/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js new file mode 100644 index 0000000000..e27d7c3b96 --- /dev/null +++ b/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js @@ -0,0 +1,35 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// The Base MerklPoolBoosterBribesModule is admined by the multichainStrategist +// 2/8 Safe (its DEFAULT_ADMIN_ROLE holder), so granting OPERATOR_ROLE to the new +// Talos signer is a plain Safe transaction +module.exports = deploymentWithGnosisSafe( + { + deployName: "052_migrate_base_merkl_module_to_talos", + safe: addresses.multichainStrategist, + network: "base", + forceDeploy: false, + }, + async () => { + const cMerklModule = await ethers.getContractAt( + "MerklPoolBoosterBribesModule", + addresses.base.MerklPoolBoosterBribesModule + ); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + name: "Grant the OPERATOR_ROLE of the Base MerklPoolBoosterBribesModule to the new Talos signer.", + actions: [ + { + contract: cMerklModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + ], + }; + } +); diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index 28c9699d6f..4056466bef 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -1370,9 +1370,21 @@ function deploymentWithGuardianGovernor(opts, fn) { function deploymentWithGnosisSafe(opts, fn) { const { deployName, dependencies, forceDeploy, onlyOnFork, forceSkip } = opts; const optsSafe = opts.safe; + // Target network the Safe lives on; gates the real-deploy run + skip. Defaults + // to mainnet so existing mainnet deploys are unaffected. + const targetNetwork = opts.network || "mainnet"; const runDeployment = async (hre) => { - const assetAddresses = await getAssetAddresses(hre.deployments); + // getAssetAddresses is mainnet-centric (resolves mock assets); on L2s it can + // throw. Safe-batch deploys don't need it, so tolerate failure. + let assetAddresses = {}; + try { + assetAddresses = await getAssetAddresses(hre.deployments); + } catch (e) { + log( + `getAssetAddresses unavailable (${e.message}); continuing without it.` + ); + } const proposal = await fn({ assetAddresses, deployWithConfirmation, @@ -1414,9 +1426,9 @@ function deploymentWithGnosisSafe(opts, fn) { fs.writeFileSync(filePath, JSON.stringify(safeJson, null, 2)); console.log(`Safe batch JSON written to ${filePath}`); - if (isMainnet) { - // Mainnet: JSON only. The operator imports it into the Safe Transaction - // Builder for the Safe to execute. + if (!isFork) { + // Real deploy: JSON only. The operator imports it into the Safe + // Transaction Builder for the Safe to execute. console.log( `Import ${deployName}.json into the Gnosis Safe (${safeAddress}) Transaction Builder to execute.` ); @@ -1452,6 +1464,11 @@ function deploymentWithGnosisSafe(opts, fn) { main.id = deployName; main.dependencies = dependencies; + // L2 fixtures filter deploys by network tag (deployments.fixture(["base"])); + // mainnet's fixture runs all deploys untagged, so only tag for non-mainnet. + if (targetNetwork !== "mainnet") { + main.tags = [targetNetwork]; + } if (forceSkip) { main.skip = () => true; } else if (forceDeploy) { @@ -1463,7 +1480,13 @@ function deploymentWithGnosisSafe(opts, fn) { const migrations = require(`./../deployments/${networkName}/.migrations.json`); return Boolean(migrations[deployName]); } else { - return onlyOnFork ? true : !isMainnet || isSmokeTest; + const onTarget = + targetNetwork === "base" + ? isBase + : targetNetwork === "sonic" + ? isSonic + : isMainnet; + return onlyOnFork ? true : isSmokeTest || !onTarget; } }; } From a4294d9c3b521478391f323392efdccff1d78def Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 4 Jun 2026 19:19:46 +0200 Subject: [PATCH 7/9] add remaining migrations for base and one for sonic --- ...ant_base_claim_bribes_module1_talos_kms.js | 33 +++++++++++++ ...ant_base_claim_bribes_module3_talos_kms.js | 33 +++++++++++++ ...30_migrate_sonic_operators_to_talos_kms.js | 49 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js create mode 100644 contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js create mode 100644 contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js diff --git a/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js b/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js new file mode 100644 index 0000000000..6655cc4547 --- /dev/null +++ b/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js @@ -0,0 +1,33 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// ClaimBribesSafeModule1 is admined by the ClaimBribes 2/8 Safe (its +// DEFAULT_ADMIN_ROLE holder and immutable safeContract()), so granting +// OPERATOR_ROLE to the new Talos signer is a plain Safe transaction. +module.exports = deploymentWithGnosisSafe( + { + deployName: "053_grant_base_claim_bribes_module1_talos", + network: "base", + forceDeploy: false, + }, + async () => { + const cModule = await ethers.getContract("ClaimBribesSafeModule1"); + const safe = await cModule.safeContract(); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + safe, + name: "Grant the OPERATOR_ROLE of the Base ClaimBribesSafeModule1 to the new Talos signer.", + actions: [ + { + contract: cModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + ], + }; + } +); diff --git a/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js b/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js new file mode 100644 index 0000000000..3059947612 --- /dev/null +++ b/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js @@ -0,0 +1,33 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// ClaimBribesSafeModule3 is admined by the base.strategist 1/2 Safe (its +// DEFAULT_ADMIN_ROLE holder and immutable safeContract()), so granting +// OPERATOR_ROLE to the new Talos signer is a plain Safe transaction. +module.exports = deploymentWithGnosisSafe( + { + deployName: "054_grant_base_claim_bribes_module3_talos", + network: "base", + forceDeploy: false, + }, + async () => { + const cModule = await ethers.getContract("ClaimBribesSafeModule3"); + const safe = await cModule.safeContract(); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + safe, + name: "Grant the OPERATOR_ROLE of the Base ClaimBribesSafeModule3 to the new Talos signer.", + actions: [ + { + contract: cModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + ], + }; + } +); diff --git a/contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js b/contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js new file mode 100644 index 0000000000..0cbc4561f7 --- /dev/null +++ b/contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js @@ -0,0 +1,49 @@ +const addresses = require("../../utils/addresses"); +const { deployOnSonic } = require("../../utils/deploy-l2"); + +// Migrate the Sonic operator/registrator roles from the old relayer EOA to the +// new Talos KMS signer. All three actions are governor-gated and executed via +// the Sonic Timelock (scheduled by the Sonic 5/8 admin). The vault rebase is +// operator-gated and currently paused, so we also unpause it once the operator +// is set. +module.exports = deployOnSonic( + { + deployName: "030_migrate_sonic_operators_to_talos", + }, + async ({ ethers }) => { + const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); + const cOSonicVault = await ethers.getContractAt( + "IVault", + cOSonicVaultProxy.address + ); + + const cSonicStakingStrategyProxy = await ethers.getContract( + "SonicStakingStrategyProxy" + ); + const cSonicStakingStrategy = await ethers.getContractAt( + "SonicStakingStrategy", + cSonicStakingStrategyProxy.address + ); + + return { + name: "Migrate Sonic operators to the Talos KMS signer", + actions: [ + { + contract: cOSonicVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOSonicVault, + signature: "unpauseRebase()", + args: [], + }, + { + contract: cSonicStakingStrategy, + signature: "setRegistrator(address)", + args: [addresses.talosRelayer], + }, + ], + }; + } +); From fbb3b72d5adc621e532e34bf4b62c59ea12a0d3a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 4 Jun 2026 22:16:10 +0200 Subject: [PATCH 8/9] add hyperevm migration --- ...igrate_crosschain_strategy_to_talos_kms.js | 30 +++++++++++++++++++ contracts/test/_fixture-hyperevm.js | 6 +++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js diff --git a/contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js b/contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js new file mode 100644 index 0000000000..34170e007e --- /dev/null +++ b/contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js @@ -0,0 +1,30 @@ +const { deployOnHyperEVM } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); + +// Re-point the HyperEVM CrossChainRemoteStrategy operator to the new Talos KMS +// signer (from the old relayer 0xC79A…0517). setOperator is onlyGovernor and the +// strategy's governor is the HyperEVM Timelock, so this is executed via that +// timelock (scheduled/executed by the hyperevm 5/8 admin). deployOnHyperEVM +// writes the schedule + execute Safe Transaction Builder JSON for the 5/8 admin. +module.exports = deployOnHyperEVM( + { + deployName: "003_migrate_crosschain_strategy_to_talos", + }, + async () => { + const cCrossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + addresses.hyperevm.CrossChainRemoteStrategy + ); + + return { + name: "Migrate the HyperEVM CrossChainRemoteStrategy operator to the new Talos signer.", + actions: [ + { + contract: cCrossChainRemoteStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + ], + }; + } +); diff --git a/contracts/test/_fixture-hyperevm.js b/contracts/test/_fixture-hyperevm.js index 4717489092..88ae881501 100644 --- a/contracts/test/_fixture-hyperevm.js +++ b/contracts/test/_fixture-hyperevm.js @@ -105,7 +105,11 @@ const crossChainHyperEVMFixture = deployments.createFixture(async () => { addresses.CCTPTokenMessengerV2 ); - const relayer = await impersonateAndFund(addresses.hyperevm.OZRelayerAddress); + // The cross-chain operator is re-pointed during the Talos signer migration + // (deploy 003), so read it from the strategy instead of hardcoding a relayer. + const relayer = await impersonateAndFund( + await fixture.crossChainRemoteStrategy.operator() + ); return { ...fixture, From 98ecdaa79f028e3c1f48fe0ad1d2548dad597350 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 5 Jun 2026 15:56:42 +0200 Subject: [PATCH 9/9] remove the old relayer roles --- ..._migrate_base_merkl_module_to_talos_kms.js | 8 ++++++- ...ant_base_claim_bribes_module1_talos_kms.js | 8 ++++++- ...ant_base_claim_bribes_module3_talos_kms.js | 8 ++++++- .../197_migrate_xogn_module6_to_talos_kms.js | 8 ++++++- ...migrate_strategist_modules_to_talos_kms.js | 21 +++++++++++++------ 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js b/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js index e27d7c3b96..6d4e327413 100644 --- a/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js +++ b/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js @@ -22,13 +22,19 @@ module.exports = deploymentWithGnosisSafe( ); return { - name: "Grant the OPERATOR_ROLE of the Base MerklPoolBoosterBribesModule to the new Talos signer.", + name: "Grant the OPERATOR_ROLE of the Base MerklPoolBoosterBribesModule to the new Talos signer, and revoke it from the old relayer.", actions: [ { contract: cMerklModule, signature: "grantRole(bytes32,address)", args: [OPERATOR_ROLE, addresses.talosRelayer], }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cMerklModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.base.OZRelayerAddress], + }, ], }; } diff --git a/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js b/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js index 6655cc4547..66f42989ba 100644 --- a/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js +++ b/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js @@ -20,13 +20,19 @@ module.exports = deploymentWithGnosisSafe( return { safe, - name: "Grant the OPERATOR_ROLE of the Base ClaimBribesSafeModule1 to the new Talos signer.", + name: "Grant the OPERATOR_ROLE of the Base ClaimBribesSafeModule1 to the new Talos signer, and revoke it from the old relayer.", actions: [ { contract: cModule, signature: "grantRole(bytes32,address)", args: [OPERATOR_ROLE, addresses.talosRelayer], }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.base.OZRelayerAddress], + }, ], }; } diff --git a/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js b/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js index 3059947612..64dbb3f406 100644 --- a/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js +++ b/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js @@ -20,13 +20,19 @@ module.exports = deploymentWithGnosisSafe( return { safe, - name: "Grant the OPERATOR_ROLE of the Base ClaimBribesSafeModule3 to the new Talos signer.", + name: "Grant the OPERATOR_ROLE of the Base ClaimBribesSafeModule3 to the new Talos signer, and revoke it from the old relayer.", actions: [ { contract: cModule, signature: "grantRole(bytes32,address)", args: [OPERATOR_ROLE, addresses.talosRelayer], }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.base.OZRelayerAddress], + }, ], }; } diff --git a/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js index 6567d1d144..54121e3ed2 100644 --- a/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js +++ b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js @@ -18,13 +18,19 @@ module.exports = deploymentWithGnosisSafe( ); return { - name: "Grant the OPERATOR_ROLE of CollectXOGNRewardsModule6 to the new Talos signer.", + name: "Grant the OPERATOR_ROLE of CollectXOGNRewardsModule6 to the new Talos signer, and revoke it from the old relayer.", actions: [ { contract: cCollectXOGNRewardsModule6, signature: "grantRole(bytes32,address)", args: [OPERATOR_ROLE, addresses.talosRelayer], }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cCollectXOGNRewardsModule6, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.mainnet.validatorRegistrator], + }, ], }; } diff --git a/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js b/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js index c33de53bd3..fa5b966f57 100644 --- a/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js +++ b/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js @@ -25,12 +25,21 @@ module.exports = deploymentWithGnosisSafe( ); return { - name: "Grant the OPERATOR_ROLE of all strategist safe modules to the new Talos signer.", - actions: modules.map((cModule) => ({ - contract: cModule, - signature: "grantRole(bytes32,address)", - args: [OPERATOR_ROLE, addresses.talosRelayer], - })), + name: "Grant the OPERATOR_ROLE of all strategist safe modules to the new Talos signer, and revoke it from the old relayer.", + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so for each + // module grant the new signer AND revoke the old relayer. + actions: modules.flatMap((cModule) => [ + { + contract: cModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + { + contract: cModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.mainnet.validatorRegistrator], + }, + ]), }; } );