From 0b607bb7ba6a1df3baedc14a3a6fcef1644eb181 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Tue, 5 May 2026 14:50:18 -0300 Subject: [PATCH 01/35] Add deploy + setKeeperRegistry changesets --- deployment/go.mod | 2 +- deployment/go.sum | 4 +- .../vault/changeset/deploy_ethbalmon.go | 495 ++++++++++++++++++ .../vault/changeset/deploy_ethbalmon_test.go | 114 ++++ .../changeset/setKeeperRegistryAddress.go | 225 ++++++++ deployment/vault/changeset/types/types.go | 27 + deployment/vault/changeset/validation.go | 48 ++ 7 files changed, 912 insertions(+), 3 deletions(-) create mode 100644 deployment/vault/changeset/deploy_ethbalmon.go create mode 100644 deployment/vault/changeset/deploy_ethbalmon_test.go create mode 100644 deployment/vault/changeset/setKeeperRegistryAddress.go diff --git a/deployment/go.mod b/deployment/go.mod index f6637deaa92..c199e677e5e 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -48,7 +48,7 @@ require ( github.com/smartcontractkit/chainlink-deployments-framework v0.94.1 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260410162948-2dca02f24e98 github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 - github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd + github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260416124344-dbcbab4eb90f github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260409211238-5b99921cbc7c github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 diff --git a/deployment/go.sum b/deployment/go.sum index a1997003205..659b81eed23 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1412,8 +1412,8 @@ github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260410162948-2dca02f24e98 h github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260410162948-2dca02f24e98/go.mod h1:6vCMfxz7cMW0wWseNKtct+b1JJbbRVJJhh/t6pQWN3M= github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 h1:QJiXTG9CmaQAuMRn5JGi+Jhji7fSkehVnKpjc8oNJJY= github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501/go.mod h1:4cT1BeNF8DAn6In9zr3LayVCv1KzFeuxT7zcuNkfIb0= -github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd h1:sK+pK4epQp20yQ7XztwrVgkTkRAr4FY+TvEegW8RuQk= -github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd/go.mod h1:7Jlt72+V9891y3LnGwHzmQwt9tfEGYryRKiGlQHo/o8= +github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260416124344-dbcbab4eb90f h1:61xS/BGIDb4qDAE6D6Yz/rOfZKBElJ0T1YL8XHpyXmI= +github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260416124344-dbcbab4eb90f/go.mod h1:a260YnLyWq2NHLUN5cSVyMGk9nhO6RguCaTI2rsVqyA= github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135 h1:8u9xUrC+yHrTDexOKDd+jrA6LCzFFHeX1G82oj2fsSI= github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135/go.mod h1:NkvE4iQgiT7dMCP6U3xPELHhWhN5Xr6rHC0axRebyMU= github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563 h1:ACpDbAxG4fa4sA83dbtYcrnlpE/y7thNIZfHxTv2ZLs= diff --git a/deployment/vault/changeset/deploy_ethbalmon.go b/deployment/vault/changeset/deploy_ethbalmon.go new file mode 100644 index 00000000000..29079d72bb4 --- /dev/null +++ b/deployment/vault/changeset/deploy_ethbalmon.go @@ -0,0 +1,495 @@ +package changeset + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/common" + + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" + "github.com/smartcontractkit/mcms" + + ds "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" + vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +const defaultEthBalMonMinWaitPeriodSeconds uint64 = 60 + +var DeployEthBalMonChangeSet cldf.ChangeSetV2[vaulttypes.DeployEthBalMonInput] = deployEthBalMon{} + +type deployEthBalMon struct { +} + +func effectiveMinWaitPeriodSeconds(v uint64) uint64 { + if v == 0 { + return defaultEthBalMonMinWaitPeriodSeconds + } + return v +} + +func mustGetContractAddress(store ds.DataStore, chainSelector uint64, contractType cldf.ContractType) (string, error) { + addr, err := GetContractAddress(store, chainSelector, contractType) + if err != nil { + return "", fmt.Errorf("failed to get contract address for type %s on chain %d: %w", contractType, chainSelector, err) + } + if addr == "" { + return "", fmt.Errorf("empty contract address for type %s on chain %d", contractType, chainSelector) + } + return addr, nil +} + +func (d deployEthBalMon) VerifyPreconditions(env cldf.Environment, config vaulttypes.DeployEthBalMonInput) error { + return ValidateDeployEthBalMonConfig(env.GetContext(), env, config) +} + +func (d deployEthBalMon) Apply(e cldf.Environment, config vaulttypes.DeployEthBalMonInput) (cldf.ChangesetOutput, error) { + logger := e.Logger + logger.Infow("Deploying Ethereum Balances Monitor", + "numChains", len(config.Chains)) + + evmChains := e.BlockChains.EVMChains() + + // Pick the first chain for deps (only needed for direct execution, not MCMS) + var primaryChain cldf_evm.Chain + for chainSelector := range config.Chains { + primaryChain = evmChains[chainSelector] + break + } + + deps := VaultDeps{ + Auth: primaryChain.DeployerKey, + Chain: primaryChain, + Environment: e, + DataStore: e.DataStore, + } + seqInput := DeployEthBalMonSequenceInput{ + Chains: config.Chains, + } + + seqReport, err := operations.ExecuteSequence(e.OperationsBundle, DeployEthBalMonSequence, deps, seqInput) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to deploy ethereum balance monitor contract sequence: %w", err) + } + + logger.Infow("ethbalmon contract deployed successfully", + "chains", len(config.Chains)) + + seqOut := seqReport.Output + memoryDataStore := ds.NewMemoryDataStore() + contractsByChain := make(map[uint64]string) + + for _, chainOut := range seqOut.Chains { + contractsByChain[chainOut.ChainSelector] = chainOut.ContractAddress + + addressRef := ds.AddressRef{ + ChainSelector: chainOut.ChainSelector, + Address: chainOut.ContractAddress, + Type: "EthBalMon", + Version: semver.MustParse("1.0.0"), + Qualifier: fmt.Sprintf("%s:%s", "EthBalMon", chainOut.ContractAddress), + Labels: ds.NewLabelSet( + "EthBalMon", + "EthBalMonV1_0_0", + ), + } + + contractMetadata := ds.ContractMetadata{ + ChainSelector: chainOut.ChainSelector, + Address: chainOut.ContractAddress, + Metadata: map[string]any{ + "deployTxHash": chainOut.DeployTxHash, + "deployBlockNumber": chainOut.DeployBlockNumber, + "keeperRegistryAddress": chainOut.KeeperRegistryAddress, + "minWaitPeriodSeconds": chainOut.MinWaitPeriodSeconds, + "timelockAddress": chainOut.TimelockAddress, + "mcmsAddress": chainOut.MCMSAddress, + "transferOwnershipTxHash": chainOut.TransferOwnershipTxHash, + }, + } + + if err := memoryDataStore.Addresses().Add(addressRef); err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to add address ref for chain %d: %w", chainOut.ChainSelector, err) + } + if err := memoryDataStore.ContractMetadata().Add(contractMetadata); err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to add contract metadata for chain %d: %w", chainOut.ChainSelector, err) + } + + } + + proposal, err := BuildAcceptOwnershipTimelockProposal( + e, + AcceptOwnershipProposalInput{ + ContractsByChain: contractsByChain, + Description: "Accept ownership of EthBalanceMonitor across chains", + Action: mcmstypes.TimelockActionBypass, + }, + ) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to build accept ownership proposal: %w", err) + } + + logger.Infow("Ethereum Balance Monitor deployment completed successfully", + "chains", len(seqOut.Chains), + ) + return cldf.ChangesetOutput{ + DataStore: memoryDataStore, + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil +} + +// ================================================ +// ================================================ +// Deploy Ethereum Balance Monitor SEQUENCE +// ================================================ +// ================================================ + +type DeployEthBalMonSequenceInput struct { + Chains map[uint64]vaulttypes.DeployEthBalMonChainConfig `json:"chains"` +} +type DeployEthBalMonPerChainOutput struct { + ChainSelector uint64 + ContractAddress string + DeployTxHash string + DeployBlockNumber uint64 + KeeperRegistryAddress string + MinWaitPeriodSeconds uint64 + TimelockAddress string + MCMSAddress string + TransferOwnershipTxHash string +} + +type DeployEthBalMonSequenceOutput struct { + Chains []DeployEthBalMonPerChainOutput +} + +var DeployEthBalMonSequence = operations.NewSequence( + "deploy-ethbalmon-sequence", + semver.MustParse("1.0.0"), + "Deploy ethereum balance monitor contracts and transfer ownership", + func(b operations.Bundle, deps VaultDeps, input DeployEthBalMonSequenceInput) (DeployEthBalMonSequenceOutput, error) { + b.Logger.Infow("Starting deploy ethbalmon contract sequence", + "chains", len(input.Chains), + ) + out := DeployEthBalMonSequenceOutput{ + Chains: []DeployEthBalMonPerChainOutput{}, + } + + evmChains := deps.Environment.BlockChains.EVMChains() + for chainSelector, chainConfig := range input.Chains { + _, ok := evmChains[chainSelector] + if !ok { + return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain not found in environment: %d", chainSelector) + } + minWait := effectiveMinWaitPeriodSeconds(*chainConfig.SetMinWaitPeriodSeconds) + timelockAddr, err := mustGetContractAddress(deps.DataStore, chainSelector, commontypes.RBACTimelock) + if err != nil { + return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain %d: failed to get timelock address: %w", chainSelector, err) + } + mcmsAddr, err := mustGetContractAddress(deps.DataStore, chainSelector, commontypes.ManyChainMultisig) + if err != nil { + return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain %d: failed to get mcms address: %w", chainSelector, err) + } + deployReport, err := operations.ExecuteOperation( + b, + DeployEthBalMonContractOperation, + deps, + DeployEthBalMonContractInput{ + ChainSelector: chainSelector, + KeeperRegistryAddress: chainConfig.SetKeeperRegistryAddress, + MinWaitPeriodSeconds: minWait, + }, + ) + if err != nil { + return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain %d: deploy operation failed: %w", chainSelector, err) + } + deployOut := deployReport.Output + transferReport, err := operations.ExecuteOperation( + b, + TransferOwnershipOperation, + deps, + TransferEthBalMonOwnershipInput{ + ChainSelector: chainSelector, + ContractAddress: deployOut.ContractAddress, + TimelockAddress: timelockAddr, + }, + ) + if err != nil { + return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain %d: transfer ownership operation failed: %w", chainSelector, err) + } + transferOut := transferReport.Output + + out.Chains = append(out.Chains, DeployEthBalMonPerChainOutput{ + ChainSelector: chainSelector, + ContractAddress: deployOut.ContractAddress, + DeployTxHash: deployOut.TxHash, + DeployBlockNumber: deployOut.BlockNumber, + KeeperRegistryAddress: deployOut.KeeperRegistryAddress, + MinWaitPeriodSeconds: deployOut.MinWaitPeriodSeconds, + TimelockAddress: timelockAddr, + MCMSAddress: mcmsAddr, + TransferOwnershipTxHash: transferOut.TxHash, + }) + } + return out, nil + }, +) + +// ================================================ +// ================================================ +// Deploy Ethereum Balance Monitor OPERATION +// ================================================ +// ================================================ + +type DeployEthBalMonContractInput struct { + ChainSelector uint64 `json:"chain_selector"` + KeeperRegistryAddress string `json:"keeper_registry_address"` + MinWaitPeriodSeconds uint64 `json:"min_wait_period_seconds"` +} + +type DeployEthBalMonContractOutput struct { + ChainSelector uint64 `json:"chain_selector"` + ContractAddress string `json:"contract_address"` + TxHash string `json:"tx_hash"` + BlockNumber uint64 `json:"block_number"` + KeeperRegistryAddress string `json:"keeper_registry_address"` + MinWaitPeriodSeconds uint64 `json:"min_wait_period_seconds"` +} + +var DeployEthBalMonContractOperation = operations.NewOperation( + "deploy-ethbalmon-contract", + semver.MustParse("1.0.0"), + "Deploy the Ethereum Balance Monitor contract", + func(b operations.Bundle, deps VaultDeps, input DeployEthBalMonContractInput) (DeployEthBalMonContractOutput, error) { + chain, ok := deps.Environment.BlockChains.EVMChains()[input.ChainSelector] + if !ok { + return DeployEthBalMonContractOutput{}, fmt.Errorf("chain not found in environment: %d", input.ChainSelector) + } + + keeperRegistryAddress := common.HexToAddress(input.KeeperRegistryAddress) + + b.Logger.Infow("Deploying EthBalanceMonitor", + "chainSelector", input.ChainSelector, + "keeperRegistryAddress", keeperRegistryAddress.Hex(), + "minWaitPeriodSeconds", input.MinWaitPeriodSeconds, + ) + + ethBalMonAddr, tx, _, err := eth_balance_monitor_wrapper.DeployEthBalanceMonitor( + chain.DeployerKey, + chain.Client, + keeperRegistryAddress, + new(big.Int).SetUint64(uint64(input.MinWaitPeriodSeconds)), // uint -> int -> bigint + ) + if err != nil { + return DeployEthBalMonContractOutput{}, fmt.Errorf("failed to deploy EthBalanceMonitor: %w", err) + } + + blockNumber, err := chain.Confirm(tx) + if err != nil { + return DeployEthBalMonContractOutput{}, fmt.Errorf("failed to confirm deploy tx %s: %w", tx.Hash().Hex(), err) + } + + out := DeployEthBalMonContractOutput{ + ChainSelector: input.ChainSelector, + ContractAddress: ethBalMonAddr.Hex(), + TxHash: tx.Hash().Hex(), + BlockNumber: blockNumber, + KeeperRegistryAddress: keeperRegistryAddress.Hex(), + MinWaitPeriodSeconds: input.MinWaitPeriodSeconds, + } + + b.Logger.Infow("EthBalanceMonitor deployed successfully", + "chainSelector", input.ChainSelector, + "contractAddress", out.ContractAddress, + "txHash", out.TxHash, + "blockNumber", out.BlockNumber, + ) + + return out, nil + + }, +) + +// ================================================ +// ================================================ +// Transfer ownership out of KMS OPERATION +// ================================================ +// ================================================ + +type TransferEthBalMonOwnershipInput struct { + ChainSelector uint64 `json:"chain_selector"` + ContractAddress string `json:"contract_address"` + TimelockAddress string `json:"timelock_address"` +} + +type TransferEthBalMonOwnershipOutput struct { + ChainSelector uint64 `json:"chain_selector"` + ContractAddress string `json:"contract_address"` + TimelockAddress string `json:"timelock_address"` + TxHash string `json:"tx_hash"` +} + +var TransferOwnershipOperation = operations.NewOperation( + "transfer-ownership", + semver.MustParse("1.0.0"), + "Transfer contract ownership out of KMS to Timelock", + func(b operations.Bundle, deps VaultDeps, input TransferEthBalMonOwnershipInput) (TransferEthBalMonOwnershipOutput, error) { + chain, ok := deps.Environment.BlockChains.EVMChains()[input.ChainSelector] + if !ok { + return TransferEthBalMonOwnershipOutput{}, fmt.Errorf("chain not found in environment: %d", input.ChainSelector) + } + + ethBalMon, err := eth_balance_monitor_wrapper.NewEthBalanceMonitor( + common.HexToAddress(input.ContractAddress), + chain.Client, + ) + if err != nil { + return TransferEthBalMonOwnershipOutput{}, fmt.Errorf("failed to instantiate EthBalanceMonitor at %s: %w", input.ContractAddress, err) + } + + b.Logger.Infow("Transferring EthBalanceMonitor ownership", + "chainSelector", input.ChainSelector, + "contractAddress", input.ContractAddress, + "timelockAddress", input.TimelockAddress, + ) + + tx, err := ethBalMon.TransferOwnership( + chain.DeployerKey, + common.HexToAddress(input.TimelockAddress), + ) + if err != nil { + return TransferEthBalMonOwnershipOutput{}, fmt.Errorf("failed to transfer ownership: %w", err) + } + + if _, err := chain.Confirm(tx); err != nil { + return TransferEthBalMonOwnershipOutput{}, fmt.Errorf("failed to confirm transfer ownership tx %s: %w", tx.Hash().Hex(), err) + } + + out := TransferEthBalMonOwnershipOutput{ + ChainSelector: input.ChainSelector, + ContractAddress: input.ContractAddress, + TimelockAddress: input.TimelockAddress, + TxHash: tx.Hash().Hex(), + } + + b.Logger.Infow("EthBalanceMonitor ownership transferred successfully", + "chainSelector", input.ChainSelector, + "contractAddress", input.ContractAddress, + "timelockAddress", input.TimelockAddress, + "txHash", out.TxHash, + ) + + return out, nil + + }, +) + +// ====================================================== +// ====================================================== +// Operation 3: Build accept ownership batch OPERATION +// ====================================================== +// ====================================================== + +type AcceptOwnershipProposalInput struct { + ContractsByChain map[uint64]string + Description string + Action mcmstypes.TimelockAction +} + +func BuildAcceptOwnershipTimelockProposal( + e cldf.Environment, + input AcceptOwnershipProposalInput, +) (*mcms.TimelockProposal, error) { + if len(input.ContractsByChain) == 0 { + return nil, fmt.Errorf("no contracts provided to build accept ownership proposal") + } + + var batches []mcmstypes.BatchOperation + timelockAddresses := make(map[mcmstypes.ChainSelector]string) + chainMetadata := make(map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata) + + for chainSelector, contractAddr := range input.ContractsByChain { + chain, ok := e.BlockChains.EVMChains()[chainSelector] + if !ok { + return nil, fmt.Errorf("chain not found in environment: %d", chainSelector) + } + + timelockAddr, err := mustGetContractAddress( + e.DataStore, + chainSelector, + commontypes.RBACTimelock, + ) + if err != nil { + return nil, fmt.Errorf("chain %d: %w", chainSelector, err) + } + + mcmsAddr, err := mustGetContractAddress( + e.DataStore, + chainSelector, + commontypes.ManyChainMultisig, + ) + if err != nil { + return nil, fmt.Errorf("chain %d: %w", chainSelector, err) + } + + ethBalMon, err := eth_balance_monitor_wrapper.NewEthBalanceMonitor( + common.HexToAddress(contractAddr), + chain.Client, + ) + if err != nil { + return nil, fmt.Errorf("chain %d: failed to instantiate EthBalanceMonitor at %s: %w", chainSelector, contractAddr, err) + } + + acceptOwnershipTx, err := ethBalMon.AcceptOwnership(cldf.SimTransactOpts()) + if err != nil { + return nil, fmt.Errorf("chain %d: failed to generate acceptOwnership calldata: %w", chainSelector, err) + } + + batches = append(batches, mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(chainSelector), + Transactions: []mcmstypes.Transaction{ + { + OperationMetadata: mcmstypes.OperationMetadata{ + ContractType: "EthBalMon", + Tags: []string{"acceptOwnership"}, + }, + To: contractAddr, + Data: acceptOwnershipTx.Data(), + AdditionalFields: json.RawMessage(`{"value": 0}`), + }, + }, + }) + + timelockAddresses[mcmstypes.ChainSelector(chainSelector)] = timelockAddr + chainMetadata[mcmstypes.ChainSelector(chainSelector)] = mcmstypes.ChainMetadata{ + StartingOpCount: 0, + MCMAddress: mcmsAddr, + } + } + + description := input.Description + if description == "" { + description = "Accept ownership of EthBalanceMonitor across chains" + } + + proposal, err := mcms.NewTimelockProposalBuilder(). + SetVersion("v1"). + SetAction(input.Action). + SetTimelockAddresses(timelockAddresses). + SetChainMetadata(chainMetadata). + SetOperations(batches). + SetDescription(description). + Build() + + if err != nil { + return nil, fmt.Errorf("failed to build timelock proposal: %w", err) + } + + return proposal, nil +} diff --git a/deployment/vault/changeset/deploy_ethbalmon_test.go b/deployment/vault/changeset/deploy_ethbalmon_test.go new file mode 100644 index 00000000000..fe113ab17fd --- /dev/null +++ b/deployment/vault/changeset/deploy_ethbalmon_test.go @@ -0,0 +1,114 @@ +package changeset + +import ( + "fmt" + "math" + "testing" + + chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" + "github.com/stretchr/testify/require" +) + +func TestDeployEthBalMonValidation(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + + env, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, []uint64{selector}), + ) + require.NoError(t, err) + + tests := []struct { + name string + config types.DeployEthBalMonInput + wantError bool + errorMsg string + }{ + { + name: "empty chains", + config: types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{}, + }, + wantError: true, + errorMsg: "chains must not be empty", + }, + { + name: "unknown chain selector", + config: types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + math.MaxUint64: { + SetKeeperRegistryAddress: "0x1234567890123456789012345678901234567890", + }, + }, + }, + wantError: true, + errorMsg: fmt.Sprintf("unknown chain selector %d", uint64(math.MaxUint64)), + }, + { + name: "empty setKeeperRegistryAddress", + config: types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: { + SetKeeperRegistryAddress: "", + }, + }, + }, + wantError: true, + errorMsg: "setKeeperRegistryAddress must not be empty", + }, + { + name: "invalid setKeeperRegistryAddress", + config: types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: { + SetKeeperRegistryAddress: "not-a-valid-address", + }, + }, + }, + wantError: true, + errorMsg: fmt.Sprintf("chain %d: setKeeperRegistryAddress is not a valid hex address: not-a-valid-address", selector), + }, + { + name: "valid config", + config: types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: { + SetKeeperRegistryAddress: "0x1234567890123456789012345678901234567890", + }, + }, + }, + wantError: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + err := ValidateDeployEthBalMonConfig(env.GetContext(), *env, test.config) + + if test.wantError { + require.Error(t, err) + if test.errorMsg != "" { + require.Contains(t, err.Error(), test.errorMsg) + } + } else { + require.NoError(t, err) + } + }) + } +} + +func TestDeployEthBalMonChangeset(t *testing.T) { + t.Parallel() + // rt, err := runtime.New(t.Context(), runtime.WithEnvOpts()) + // require.NoError(t, err) + + t.Run("single chain", func(t *testing.T) { + t.Parallel() + + }) +} diff --git a/deployment/vault/changeset/setKeeperRegistryAddress.go b/deployment/vault/changeset/setKeeperRegistryAddress.go new file mode 100644 index 00000000000..a64d2a0e033 --- /dev/null +++ b/deployment/vault/changeset/setKeeperRegistryAddress.go @@ -0,0 +1,225 @@ +package changeset + +import ( + "encoding/json" + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/common" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" + vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" + "github.com/smartcontractkit/mcms" + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +type setKeeperRegistryAddress struct{} + +var SetKeeperRegistryAddress cldf.ChangeSetV2[vaulttypes.EthBalMonSetKeeperRegistryAddressInput] = setKeeperRegistryAddress{} + +func (sk setKeeperRegistryAddress) VerifyPreconditions(env cldf.Environment, config vaulttypes.EthBalMonSetKeeperRegistryAddressInput) error { + return ValidateSetKeeperRegistryAddressConfig(env.GetContext(), env, config) +} + +func (sk setKeeperRegistryAddress) Apply( + e cldf.Environment, + config vaulttypes.EthBalMonSetKeeperRegistryAddressInput, +) (cldf.ChangesetOutput, error) { + logger := e.Logger + logger.Infow("Generating SetKeeperRegistryAddress proposal for Ethereum Balance Monitor", + "numChains", len(config.Chains), + ) + + deps := VaultDeps{ + Environment: e, + DataStore: e.DataStore, + } + + seqInput := EthBalMonSetKeeperRegistryAddressSequenceInput{ + Chains: config.Chains, + } + + seqReport, err := operations.ExecuteSequence( + e.OperationsBundle, + SetKeeperRegistrySequence, + deps, + seqInput, + ) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to set keeper registry address sequence: %w", err) + } + + return cldf.ChangesetOutput{ + MCMSTimelockProposals: seqReport.Output.MCMSTimelockProposals, + }, nil +} + +type EthBalMonSetKeeperRegistryAddressSequenceInput struct { + Chains map[uint64]vaulttypes.SetKeeperRegistryChainConfig `json:"chains"` +} + +type EthBalMonSetKeeperRegistryAddressSequenceOutput struct { + MCMSTimelockProposals []mcms.TimelockProposal +} + +var SetKeeperRegistrySequence = operations.NewSequence( + "ethbalmon-set-keeper-registry", + semver.MustParse("1.0.0"), + "Generate MCMS timelock proposal to set Keeper Registry address on EthBalMon across chains", + func( + b operations.Bundle, + deps VaultDeps, + input EthBalMonSetKeeperRegistryAddressSequenceInput, + ) (EthBalMonSetKeeperRegistryAddressSequenceOutput, error) { + b.Logger.Infow("Starting EthBalMon set keeper registry sequence", + "chains", len(input.Chains), + ) + + opReport, err := operations.ExecuteOperation( + b, + SetKeeperRegistryOperation, + deps, + SetKeeperRegistryOperationInput{ + Chains: input.Chains, + }, + ) + if err != nil { + return EthBalMonSetKeeperRegistryAddressSequenceOutput{}, + fmt.Errorf("failed to generate set keeper registry proposal: %w", err) + } + + return EthBalMonSetKeeperRegistryAddressSequenceOutput{ + MCMSTimelockProposals: opReport.Output.MCMSTimelockProposals, + }, nil + }, +) + +type SetKeeperRegistryOperationInput struct { + Chains map[uint64]vaulttypes.SetKeeperRegistryChainConfig `json:"chains"` +} + +type SetKeeperRegistryOperationOutput struct { + MCMSTimelockProposals []mcms.TimelockProposal +} + +var SetKeeperRegistryOperation = operations.NewOperation( + "ethbalmon-set-keeper-registry-op", + semver.MustParse("1.0.0"), + "Generate proposal to set Keeper Registry address on the Ethereum Balance Monitor contract", + func( + b operations.Bundle, + deps VaultDeps, + input SetKeeperRegistryOperationInput, + ) (SetKeeperRegistryOperationOutput, error) { + if len(input.Chains) == 0 { + return SetKeeperRegistryOperationOutput{}, fmt.Errorf("no chains provided") + } + + var batches []mcmstypes.BatchOperation + timelockAddresses := make(map[mcmstypes.ChainSelector]string) + chainMetadata := make(map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata) + + evmChains := deps.Environment.BlockChains.EVMChains() + + for chainSelector, chainConfig := range input.Chains { + chain, ok := evmChains[chainSelector] + if !ok { + return SetKeeperRegistryOperationOutput{}, fmt.Errorf("chain not found in environment: %d", chainSelector) + } + + ethBalMonAddr, err := mustGetContractAddress( + deps.DataStore, + chainSelector, + cldf.ContractType("EthBalMon"), + ) + if err != nil { + return SetKeeperRegistryOperationOutput{}, + fmt.Errorf("chain %d: failed to get EthBalMon address: %w", chainSelector, err) + } + + timelockAddr, err := mustGetContractAddress( + deps.DataStore, + chainSelector, + commontypes.RBACTimelock, + ) + if err != nil { + return SetKeeperRegistryOperationOutput{}, + fmt.Errorf("chain %d: failed to get timelock address: %w", chainSelector, err) + } + + mcmsAddr, err := mustGetContractAddress( + deps.DataStore, + chainSelector, + commontypes.ManyChainMultisig, + ) + if err != nil { + return SetKeeperRegistryOperationOutput{}, + fmt.Errorf("chain %d: failed to get MCMS address: %w", chainSelector, err) + } + + ethBalMon, err := eth_balance_monitor_wrapper.NewEthBalanceMonitor( + common.HexToAddress(ethBalMonAddr), + chain.Client, + ) + if err != nil { + return SetKeeperRegistryOperationOutput{}, + fmt.Errorf("chain %d: failed to instantiate EthBalanceMonitor at %s: %w", chainSelector, ethBalMonAddr, err) + } + + setKeeperRegistryTx, err := ethBalMon.SetKeeperRegistryAddress( + cldf.SimTransactOpts(), + common.HexToAddress(chainConfig.NewKeeperRegistryAddress), + ) + if err != nil { + return SetKeeperRegistryOperationOutput{}, + fmt.Errorf("chain %d: failed to generate setKeeperRegistryAddress calldata: %w", chainSelector, err) + } + + batches = append(batches, mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(chainSelector), + Transactions: []mcmstypes.Transaction{ + { + OperationMetadata: mcmstypes.OperationMetadata{ + ContractType: "EthBalMon", + Tags: []string{ + "setKeeperRegistryAddress", + }, + }, + To: ethBalMonAddr, + Data: setKeeperRegistryTx.Data(), + AdditionalFields: json.RawMessage(`{"value": 0}`), + }, + }, + }) + + timelockAddresses[mcmstypes.ChainSelector(chainSelector)] = timelockAddr + chainMetadata[mcmstypes.ChainSelector(chainSelector)] = mcmstypes.ChainMetadata{ + StartingOpCount: 0, + MCMAddress: mcmsAddr, + } + } + + proposal, err := mcms.NewTimelockProposalBuilder(). + SetVersion("v1"). + SetAction(mcmstypes.TimelockActionBypass). + SetTimelockAddresses(timelockAddresses). + SetChainMetadata(chainMetadata). + SetOperations(batches). + SetDescription("Set Keeper Registry address on EthBalanceMonitor across chains"). + Build() + if err != nil { + return SetKeeperRegistryOperationOutput{}, fmt.Errorf("failed to build timelock proposal: %w", err) + } + + b.Logger.Infow("Generated EthBalMon set keeper registry proposal", + "chains", len(input.Chains), + "operations", len(batches), + ) + + return SetKeeperRegistryOperationOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil + }, +) diff --git a/deployment/vault/changeset/types/types.go b/deployment/vault/changeset/types/types.go index 31f61431c30..5f2c3b71ea7 100644 --- a/deployment/vault/changeset/types/types.go +++ b/deployment/vault/changeset/types/types.go @@ -72,3 +72,30 @@ type BatchNativeTransferState struct { // ValidationErrors contains any validation errors found ValidationErrors []TransferValidationError `json:"validation_errors"` } + +// DeployEthBalMonChainConfig is the EthBalMon configuration for a single chain. +// +// SetKeeperRegistryAddress is the Chainlink Automation registry forwarder address (from the +// upkeep "forwarder address" on chains with Chainlink automation) or the +// KMS executor address on chains that use the Plaid/KMS automation path instead. +// +// SetMinWaitPeriodSeconds is optional; when omitted or non-positive it defaults to 60. +type DeployEthBalMonChainConfig struct { + SetKeeperRegistryAddress string `json:"setKeeperRegistryAddress"` + SetMinWaitPeriodSeconds *uint64 `json:"setMinWaitPeriodSeconds,omitempty"` +} + +// DeployEthBalMonInput configures EthBalMon deployment across chains. +// Map keys are chain selectors; each entry is one chain’s keeper/KMS and watch list. +type DeployEthBalMonInput struct { + Chains map[uint64]DeployEthBalMonChainConfig `json:"chains"` +} + +// setKeeperRegistryAddress config +type SetKeeperRegistryChainConfig struct { + NewKeeperRegistryAddress string `json:"new_keeper_registry_address"` +} + +type EthBalMonSetKeeperRegistryAddressInput struct { + Chains map[uint64]SetKeeperRegistryChainConfig `json:"chains"` +} diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index ee9c1595b31..28ca0096478 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -221,3 +221,51 @@ func ValidateSetWhitelistConfig(e cldf.Environment, cfg types.SetWhitelistConfig return nil } + +func validateEthAddress(field, raw string) error { + if raw == "" { + return fmt.Errorf("%s must not be empty", field) + } + if !common.IsHexAddress(raw) { + return fmt.Errorf("%s is not a valid hex address: %s", field, raw) + } + return nil +} + +func ValidateDeployEthBalMonConfig(ctx context.Context, env cldf.Environment, cfg types.DeployEthBalMonInput) error { + if len(cfg.Chains) == 0 { + return errors.New("chains must not be empty") + } + + for chainSelector, chainCfg := range cfg.Chains { + if err := validateChainSelector(chainSelector, env); err != nil { + return fmt.Errorf("chain %d: %w", chainSelector, err) + } + if chainCfg.SetKeeperRegistryAddress == "" { + return fmt.Errorf("chain %d: setKeeperRegistryAddress must not be empty", chainSelector) + } + if err := validateEthAddress("setKeeperRegistryAddress", chainCfg.SetKeeperRegistryAddress); err != nil { + return fmt.Errorf("chain %d: %w", chainSelector, err) + } + } + + return nil +} + +func ValidateSetKeeperRegistryAddressConfig(ctx context.Context, env cldf.Environment, cfg types.EthBalMonSetKeeperRegistryAddressInput) error { + if len(cfg.Chains) == 0 { + return fmt.Errorf("no chains provided") + } + + for chainSelector, chainConfig := range cfg.Chains { + if _, ok := env.BlockChains.EVMChains()[chainSelector]; !ok { + return fmt.Errorf("chain not found in environment: %d", chainSelector) + } + + if !common.IsHexAddress(chainConfig.NewKeeperRegistryAddress) { + return fmt.Errorf("chain %d: invalid keeper registry address: %s", chainSelector, chainConfig.NewKeeperRegistryAddress) + } + } + + return nil +} From 5f11f43e266c69b11502f84436f262588b5b2ed5 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Tue, 5 May 2026 14:55:41 -0300 Subject: [PATCH 02/35] Add constant to set the eth bal mon contract type --- deployment/vault/changeset/deploy_ethbalmon.go | 8 ++++---- deployment/vault/changeset/setKeeperRegistryAddress.go | 4 ++-- deployment/vault/changeset/types/types.go | 2 ++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/deployment/vault/changeset/deploy_ethbalmon.go b/deployment/vault/changeset/deploy_ethbalmon.go index 29079d72bb4..af2ea6206ca 100644 --- a/deployment/vault/changeset/deploy_ethbalmon.go +++ b/deployment/vault/changeset/deploy_ethbalmon.go @@ -91,11 +91,11 @@ func (d deployEthBalMon) Apply(e cldf.Environment, config vaulttypes.DeployEthBa addressRef := ds.AddressRef{ ChainSelector: chainOut.ChainSelector, Address: chainOut.ContractAddress, - Type: "EthBalMon", + Type: ds.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), Version: semver.MustParse("1.0.0"), - Qualifier: fmt.Sprintf("%s:%s", "EthBalMon", chainOut.ContractAddress), + Qualifier: fmt.Sprintf("%s:%s", vaulttypes.ETHBALMON_CONTRACT_TYPE, chainOut.ContractAddress), Labels: ds.NewLabelSet( - "EthBalMon", + vaulttypes.ETHBALMON_CONTRACT_TYPE, "EthBalMonV1_0_0", ), } @@ -456,7 +456,7 @@ func BuildAcceptOwnershipTimelockProposal( Transactions: []mcmstypes.Transaction{ { OperationMetadata: mcmstypes.OperationMetadata{ - ContractType: "EthBalMon", + ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, Tags: []string{"acceptOwnership"}, }, To: contractAddr, diff --git a/deployment/vault/changeset/setKeeperRegistryAddress.go b/deployment/vault/changeset/setKeeperRegistryAddress.go index a64d2a0e033..7a77cfa4f4c 100644 --- a/deployment/vault/changeset/setKeeperRegistryAddress.go +++ b/deployment/vault/changeset/setKeeperRegistryAddress.go @@ -132,7 +132,7 @@ var SetKeeperRegistryOperation = operations.NewOperation( ethBalMonAddr, err := mustGetContractAddress( deps.DataStore, chainSelector, - cldf.ContractType("EthBalMon"), + cldf.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), ) if err != nil { return SetKeeperRegistryOperationOutput{}, @@ -182,7 +182,7 @@ var SetKeeperRegistryOperation = operations.NewOperation( Transactions: []mcmstypes.Transaction{ { OperationMetadata: mcmstypes.OperationMetadata{ - ContractType: "EthBalMon", + ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, Tags: []string{ "setKeeperRegistryAddress", }, diff --git a/deployment/vault/changeset/types/types.go b/deployment/vault/changeset/types/types.go index 5f2c3b71ea7..b1e444ebe4e 100644 --- a/deployment/vault/changeset/types/types.go +++ b/deployment/vault/changeset/types/types.go @@ -91,6 +91,8 @@ type DeployEthBalMonInput struct { Chains map[uint64]DeployEthBalMonChainConfig `json:"chains"` } +const ETHBALMON_CONTRACT_TYPE string = "EthBalMon" + // setKeeperRegistryAddress config type SetKeeperRegistryChainConfig struct { NewKeeperRegistryAddress string `json:"new_keeper_registry_address"` From 098e534474509b2f76894c1229d4bca5f878903e Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Tue, 5 May 2026 14:59:13 -0300 Subject: [PATCH 03/35] Change files names --- .../vault/changeset/{deploy_ethbalmon.go => ethbalmon_deploy.go} | 0 .../{deploy_ethbalmon_test.go => ethbalmon_deploy_test.go} | 0 deployment/vault/changeset/ethbalmon_renounce_ownership.go | 1 + ...rRegistryAddress.go => ethbalmon_setKeeperRegistryAddress.go} | 0 4 files changed, 1 insertion(+) rename deployment/vault/changeset/{deploy_ethbalmon.go => ethbalmon_deploy.go} (100%) rename deployment/vault/changeset/{deploy_ethbalmon_test.go => ethbalmon_deploy_test.go} (100%) create mode 100644 deployment/vault/changeset/ethbalmon_renounce_ownership.go rename deployment/vault/changeset/{setKeeperRegistryAddress.go => ethbalmon_setKeeperRegistryAddress.go} (100%) diff --git a/deployment/vault/changeset/deploy_ethbalmon.go b/deployment/vault/changeset/ethbalmon_deploy.go similarity index 100% rename from deployment/vault/changeset/deploy_ethbalmon.go rename to deployment/vault/changeset/ethbalmon_deploy.go diff --git a/deployment/vault/changeset/deploy_ethbalmon_test.go b/deployment/vault/changeset/ethbalmon_deploy_test.go similarity index 100% rename from deployment/vault/changeset/deploy_ethbalmon_test.go rename to deployment/vault/changeset/ethbalmon_deploy_test.go diff --git a/deployment/vault/changeset/ethbalmon_renounce_ownership.go b/deployment/vault/changeset/ethbalmon_renounce_ownership.go new file mode 100644 index 00000000000..d57836e7ec1 --- /dev/null +++ b/deployment/vault/changeset/ethbalmon_renounce_ownership.go @@ -0,0 +1 @@ +package changeset diff --git a/deployment/vault/changeset/setKeeperRegistryAddress.go b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go similarity index 100% rename from deployment/vault/changeset/setKeeperRegistryAddress.go rename to deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go From 497d267c7d3ddacce26251fd1d2372321cb70510 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Tue, 5 May 2026 16:18:12 -0300 Subject: [PATCH 04/35] Remove renounceRole logic since it is not neccesary --- .../vault/changeset/ethbalmon_renounce_ownership.go | 1 - .../changeset/ethbalmon_setKeeperRegistryAddress.go | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) delete mode 100644 deployment/vault/changeset/ethbalmon_renounce_ownership.go diff --git a/deployment/vault/changeset/ethbalmon_renounce_ownership.go b/deployment/vault/changeset/ethbalmon_renounce_ownership.go deleted file mode 100644 index d57836e7ec1..00000000000 --- a/deployment/vault/changeset/ethbalmon_renounce_ownership.go +++ /dev/null @@ -1 +0,0 @@ -package changeset diff --git a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go index 7a77cfa4f4c..6781621a386 100644 --- a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go +++ b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go @@ -6,6 +6,7 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" @@ -32,7 +33,17 @@ func (sk setKeeperRegistryAddress) Apply( "numChains", len(config.Chains), ) + evmChains := e.BlockChains.EVMChains() + + var primaryChain cldf_evm.Chain + for chainSelector := range config.Chains { + primaryChain = evmChains[chainSelector] + break + } + deps := VaultDeps{ + Auth: primaryChain.DeployerKey, + Chain: primaryChain, Environment: e, DataStore: e.DataStore, } @@ -123,6 +134,7 @@ var SetKeeperRegistryOperation = operations.NewOperation( evmChains := deps.Environment.BlockChains.EVMChains() + // Move this to the sequence for chainSelector, chainConfig := range input.Chains { chain, ok := evmChains[chainSelector] if !ok { From b5fa134aebb28eb9f8e8f8c467f33974e849176f Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Tue, 5 May 2026 17:17:35 -0300 Subject: [PATCH 05/35] Improve setKeeperRegistryAddress changeset strcuture --- .../ethbalmon_setKeeperRegistryAddress.go | 227 ++++++++++-------- 1 file changed, 121 insertions(+), 106 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go index 6781621a386..9b15588110b 100644 --- a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go +++ b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go @@ -88,150 +88,165 @@ var SetKeeperRegistrySequence = operations.NewSequence( "chains", len(input.Chains), ) - opReport, err := operations.ExecuteOperation( - b, - SetKeeperRegistryOperation, - deps, - SetKeeperRegistryOperationInput{ - Chains: input.Chains, - }, - ) + if len(input.Chains) == 0 { + return EthBalMonSetKeeperRegistryAddressSequenceOutput{}, fmt.Errorf("no chains provided") + } + + var batches []mcmstypes.BatchOperation + timelockAddresses := make(map[mcmstypes.ChainSelector]string) + chainMetadata := make(map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata) + + for chainSelector, chainConfig := range input.Chains { + opReport, err := operations.ExecuteOperation( + b, + SetKeeperRegistryOperation, + deps, + SetKeeperRegistryOperationInput{ + ChainSelector: chainSelector, + NewKeeperRegistryAddress: chainConfig.NewKeeperRegistryAddress, + }, + ) + if err != nil { + return EthBalMonSetKeeperRegistryAddressSequenceOutput{}, + fmt.Errorf("chain %d: failed to generate set keeper registry batch: %w", chainSelector, err) + } + + opOut := opReport.Output + + batches = append(batches, opOut.BatchOperation) + timelockAddresses[mcmstypes.ChainSelector(chainSelector)] = opOut.TimelockAddress + chainMetadata[mcmstypes.ChainSelector(chainSelector)] = mcmstypes.ChainMetadata{ + StartingOpCount: 0, + MCMAddress: opOut.MCMSAddress, + } + } + + proposal, err := mcms.NewTimelockProposalBuilder(). + SetVersion("v1"). + SetAction(mcmstypes.TimelockActionBypass). + SetTimelockAddresses(timelockAddresses). + SetChainMetadata(chainMetadata). + SetOperations(batches). + SetDescription("Set Keeper Registry address on EthBalanceMonitor across chains"). + Build() if err != nil { return EthBalMonSetKeeperRegistryAddressSequenceOutput{}, - fmt.Errorf("failed to generate set keeper registry proposal: %w", err) + fmt.Errorf("failed to build timelock proposal: %w", err) } + b.Logger.Infow("Generated EthBalMon set keeper registry proposal", + "chains", len(input.Chains), + "operations", len(batches), + ) + return EthBalMonSetKeeperRegistryAddressSequenceOutput{ - MCMSTimelockProposals: opReport.Output.MCMSTimelockProposals, + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, }, nil }, ) type SetKeeperRegistryOperationInput struct { - Chains map[uint64]vaulttypes.SetKeeperRegistryChainConfig `json:"chains"` + ChainSelector uint64 `json:"chain_selector"` + NewKeeperRegistryAddress string `json:"new_keeper_registry_address"` } type SetKeeperRegistryOperationOutput struct { - MCMSTimelockProposals []mcms.TimelockProposal + ChainSelector uint64 + BatchOperation mcmstypes.BatchOperation + TimelockAddress string + MCMSAddress string } var SetKeeperRegistryOperation = operations.NewOperation( "ethbalmon-set-keeper-registry-op", semver.MustParse("1.0.0"), - "Generate proposal to set Keeper Registry address on the Ethereum Balance Monitor contract", + "Generate batch operation to set Keeper Registry address on the Ethereum Balance Monitor contract", func( b operations.Bundle, deps VaultDeps, input SetKeeperRegistryOperationInput, ) (SetKeeperRegistryOperationOutput, error) { - if len(input.Chains) == 0 { - return SetKeeperRegistryOperationOutput{}, fmt.Errorf("no chains provided") + chain, ok := deps.Environment.BlockChains.EVMChains()[input.ChainSelector] + if !ok { + return SetKeeperRegistryOperationOutput{}, fmt.Errorf("chain not found in environment: %d", input.ChainSelector) } - var batches []mcmstypes.BatchOperation - timelockAddresses := make(map[mcmstypes.ChainSelector]string) - chainMetadata := make(map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata) - - evmChains := deps.Environment.BlockChains.EVMChains() - - // Move this to the sequence - for chainSelector, chainConfig := range input.Chains { - chain, ok := evmChains[chainSelector] - if !ok { - return SetKeeperRegistryOperationOutput{}, fmt.Errorf("chain not found in environment: %d", chainSelector) - } - - ethBalMonAddr, err := mustGetContractAddress( - deps.DataStore, - chainSelector, - cldf.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), - ) - if err != nil { - return SetKeeperRegistryOperationOutput{}, - fmt.Errorf("chain %d: failed to get EthBalMon address: %w", chainSelector, err) - } + ethBalMonAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + cldf.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), + ) + if err != nil { + return SetKeeperRegistryOperationOutput{}, + fmt.Errorf("failed to get EthBalMon address: %w", err) + } - timelockAddr, err := mustGetContractAddress( - deps.DataStore, - chainSelector, - commontypes.RBACTimelock, - ) - if err != nil { - return SetKeeperRegistryOperationOutput{}, - fmt.Errorf("chain %d: failed to get timelock address: %w", chainSelector, err) - } + timelockAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + commontypes.RBACTimelock, + ) + if err != nil { + return SetKeeperRegistryOperationOutput{}, + fmt.Errorf("failed to get timelock address: %w", err) + } - mcmsAddr, err := mustGetContractAddress( - deps.DataStore, - chainSelector, - commontypes.ManyChainMultisig, - ) - if err != nil { - return SetKeeperRegistryOperationOutput{}, - fmt.Errorf("chain %d: failed to get MCMS address: %w", chainSelector, err) - } + mcmsAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + commontypes.ManyChainMultisig, + ) + if err != nil { + return SetKeeperRegistryOperationOutput{}, + fmt.Errorf("failed to get MCMS address: %w", err) + } - ethBalMon, err := eth_balance_monitor_wrapper.NewEthBalanceMonitor( - common.HexToAddress(ethBalMonAddr), - chain.Client, - ) - if err != nil { - return SetKeeperRegistryOperationOutput{}, - fmt.Errorf("chain %d: failed to instantiate EthBalanceMonitor at %s: %w", chainSelector, ethBalMonAddr, err) - } + ethBalMon, err := eth_balance_monitor_wrapper.NewEthBalanceMonitor( + common.HexToAddress(ethBalMonAddr), + chain.Client, + ) + if err != nil { + return SetKeeperRegistryOperationOutput{}, + fmt.Errorf("failed to instantiate EthBalanceMonitor at %s: %w", ethBalMonAddr, err) + } - setKeeperRegistryTx, err := ethBalMon.SetKeeperRegistryAddress( - cldf.SimTransactOpts(), - common.HexToAddress(chainConfig.NewKeeperRegistryAddress), - ) - if err != nil { - return SetKeeperRegistryOperationOutput{}, - fmt.Errorf("chain %d: failed to generate setKeeperRegistryAddress calldata: %w", chainSelector, err) - } + setKeeperRegistryTx, err := ethBalMon.SetKeeperRegistryAddress( + cldf.SimTransactOpts(), + common.HexToAddress(input.NewKeeperRegistryAddress), + ) + if err != nil { + return SetKeeperRegistryOperationOutput{}, + fmt.Errorf("failed to generate setKeeperRegistryAddress calldata: %w", err) + } - batches = append(batches, mcmstypes.BatchOperation{ - ChainSelector: mcmstypes.ChainSelector(chainSelector), - Transactions: []mcmstypes.Transaction{ - { - OperationMetadata: mcmstypes.OperationMetadata{ - ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, - Tags: []string{ - "setKeeperRegistryAddress", - }, + batch := mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(input.ChainSelector), + Transactions: []mcmstypes.Transaction{ + { + OperationMetadata: mcmstypes.OperationMetadata{ + ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, + Tags: []string{ + "setKeeperRegistryAddress", }, - To: ethBalMonAddr, - Data: setKeeperRegistryTx.Data(), - AdditionalFields: json.RawMessage(`{"value": 0}`), }, + To: ethBalMonAddr, + Data: setKeeperRegistryTx.Data(), + AdditionalFields: json.RawMessage(`{"value": 0}`), }, - }) - - timelockAddresses[mcmstypes.ChainSelector(chainSelector)] = timelockAddr - chainMetadata[mcmstypes.ChainSelector(chainSelector)] = mcmstypes.ChainMetadata{ - StartingOpCount: 0, - MCMAddress: mcmsAddr, - } - } - - proposal, err := mcms.NewTimelockProposalBuilder(). - SetVersion("v1"). - SetAction(mcmstypes.TimelockActionBypass). - SetTimelockAddresses(timelockAddresses). - SetChainMetadata(chainMetadata). - SetOperations(batches). - SetDescription("Set Keeper Registry address on EthBalanceMonitor across chains"). - Build() - if err != nil { - return SetKeeperRegistryOperationOutput{}, fmt.Errorf("failed to build timelock proposal: %w", err) + }, } - b.Logger.Infow("Generated EthBalMon set keeper registry proposal", - "chains", len(input.Chains), - "operations", len(batches), + b.Logger.Infow("Generated EthBalMon set keeper registry batch", + "chainSelector", input.ChainSelector, + "ethBalMon", ethBalMonAddr, + "newKeeperRegistry", input.NewKeeperRegistryAddress, ) return SetKeeperRegistryOperationOutput{ - MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + ChainSelector: input.ChainSelector, + BatchOperation: batch, + TimelockAddress: timelockAddr, + MCMSAddress: mcmsAddr, }, nil }, ) From 97ce85d07e54948f980d2c9c55b3a9dc8a478c59 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 6 May 2026 18:05:40 -0300 Subject: [PATCH 06/35] add setWatchList changeset --- .../ethbalmon_setKeeperRegistryAddress.go | 8 +- .../vault/changeset/ethbalmon_setWatchList.go | 234 ++++++++++++++++++ deployment/vault/changeset/types/types.go | 10 + 3 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 deployment/vault/changeset/ethbalmon_setWatchList.go diff --git a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go index 9b15588110b..eef441ea9ba 100644 --- a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go +++ b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go @@ -151,10 +151,10 @@ type SetKeeperRegistryOperationInput struct { } type SetKeeperRegistryOperationOutput struct { - ChainSelector uint64 - BatchOperation mcmstypes.BatchOperation - TimelockAddress string - MCMSAddress string + ChainSelector uint64 `json:"chain_selector"` + BatchOperation mcmstypes.BatchOperation `json:"batch_operation"` + TimelockAddress string `json:"timelock_address"` + MCMSAddress string `json:"mcms_address"` } var SetKeeperRegistryOperation = operations.NewOperation( diff --git a/deployment/vault/changeset/ethbalmon_setWatchList.go b/deployment/vault/changeset/ethbalmon_setWatchList.go new file mode 100644 index 00000000000..6d537281650 --- /dev/null +++ b/deployment/vault/changeset/ethbalmon_setWatchList.go @@ -0,0 +1,234 @@ +package changeset + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/common" + "github.com/smartco ntractkit/mcms" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" + vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" + mcmssdk "github.com/smartcontractkit/mcms/sdk" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +type ethBalMonSetWatchList struct{} + +var EthBalMonSetWatchList cldf.ChangeSetV2[vaulttypes.EthBalMonSetWatchListInput] = ethBalMonSetWatchList{} + +func (sw ethBalMonSetWatchList) VerifyPreconditions(env cldf.Environment, config vaulttypes.EthBalMonSetWatchListInput) error { + return nil + +} + +func (sw ethBalMonSetWatchList) Apply(e cldf.Environment, config vaulttypes.EthBalMonSetWatchListInput) (cldf.ChangesetOutput, error) { + logger := e.Logger + logger.Infow("Generating EthBalMon setWatchList proposal", "numChains", len(config.Chains)) + + evmChains := e.BlockChains.EVMChains() + + var primaryChain cldf_evm.Chain + for chainSelector := range config.Chains { + primaryChain = evmChains[chainSelector] + break + } + + deps := VaultDeps{ + Auth: primaryChain.DeployerKey, + Chain: primaryChain, + Environment: e, + DataStore: e.DataStore, + } + + seqInput := EthBalMonSetWatchListSeqInput{ + Chains: config.Chains, + } + + seqReport, err := operations.ExecuteSequence(e.OperationsBundle, EthBalMonSetWatchListSequence, deps, seqInput) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed on EthBalMonSetWatchListSequence sequence: %w", err) + } + + return cldf.ChangesetOutput{ + MCMSTimelockProposals: seqReport.Output.MCMSTimelockProposals, + }, nil +} + +type EthBalMonSetWatchListSeqInput struct { + Chains map[uint64]vaulttypes.EthBalMonSetWatchListChainConfig `json:"chains"` +} + +type EthBalMonSetWatchListSeqOutput struct { + MCMSTimelockProposals []mcms.TimelockProposal `json:"mcms_timelock_proposals"` +} + +var EthBalMonSetWatchListSequence = operations.NewSequence( + "ethbalmon-setWathcList-sequence", + semver.MustParse("1.0.0"), + "Sequence to create operations for EthBalMon setWatchList", + func(b operations.Bundle, deps VaultDeps, input EthBalMonSetWatchListSeqInput) (EthBalMonSetWatchListSeqOutput, error) { + b.Logger.Infow("Starting EthBalMon setWatchList sequence", + "chains", len(input.Chains), + ) + var batches []mcmstypes.BatchOperation + timelockAddresses := make(map[uint64]string) + mcmAddressByChain := make(map[uint64]string) + inspectorPerChain := make(map[uint64]mcmssdk.Inspector) + + for chainSelector, chainConfig := range input.Chains { + opReport, err := operations.ExecuteOperation(b, EthBalMonSetWatchListOperation, deps, EthBalMonSetWatchListOpInput{ + ChainSelector: chainSelector, + Addresses: chainConfig.Addresses, + MinBalancesWei: chainConfig.MinBalancesWei, + TopUpAmountsWei: chainConfig.TopUpAmountsWei, + }) + opOutput := opReport.Output + if err != nil { + return EthBalMonSetWatchListSeqOutput{}, fmt.Errorf("chain %d: failed to generate setWatchList batch: %w", chainSelector, err) + } + batches = append(batches, opOutput.BatchOperation) + timelockAddresses[chainSelector] = opOutput.TimelockAddress + mcmAddressByChain[chainSelector] = opOutput.MCMSAddress + inspectorPerChain[chainSelector] = opOutput.Inspector + } + + proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, inspectorPerChain, batches, "EthBalMon SetWatchList", proposalutils.TimelockConfig{ + MinDelay: 0, + }) + + if err != nil { + return EthBalMonSetWatchListSeqOutput{}, fmt.Errorf("failed to build timelock proposal: %w", err) + } + b.Logger.Infow("Generated EthBalMon setWatchList proposal", + "chains", len(input.Chains), "operations", len(batches)) + + return EthBalMonSetWatchListSeqOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil + }, +) + +type EthBalMonSetWatchListOpInput struct { + ChainSelector uint64 `json:"chain_selector"` + Addresses []common.Address `json:"addresses"` + MinBalancesWei []big.Int `json:"min_balance_wei"` + TopUpAmountsWei []big.Int `json:"topup_amounts_wei"` +} + +type EthBalMonSetWatchListOpOutput struct { + ChainSelector uint64 `json:"chain_selector"` + BatchOperation mcmstypes.BatchOperation `json:"batch_operation"` + TimelockAddress string `json:"timelock_address"` + MCMSAddress string `json:"mcms_address"` + Inspector *mcmsevmsdk.Inspector `json:"inspector"` +} + +var EthBalMonSetWatchListOperation = operations.NewOperation( + "ethbalmon-setWathcList-operation", + semver.MustParse("1.0.0"), + "Operation to create transaction batch for EthBalMon setWatchList", + func(b operations.Bundle, deps VaultDeps, input EthBalMonSetWatchListOpInput) (EthBalMonSetWatchListOpOutput, error) { + b.Logger.Infow("Starting EthBalMon setWatchList operation", + "chainsel", input.ChainSelector, + "addresses", len(input.Addresses), + ) + chain, ok := deps.Environment.BlockChains.EVMChains()[input.ChainSelector] + + if !ok { + return EthBalMonSetWatchListOpOutput{}, fmt.Errorf("chain not found in environment: %d", input.ChainSelector) + } + + ethBalMonAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + cldf.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), + ) + if err != nil { + return EthBalMonSetWatchListOpOutput{}, + fmt.Errorf("failed to get EthBalMon address: %w", err) + } + + timelockAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + commontypes.RBACTimelock, + ) + if err != nil { + return EthBalMonSetWatchListOpOutput{}, + fmt.Errorf("failed to get timelock address: %w", err) + } + mcmsAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + commontypes.ManyChainMultisig, + ) + if err != nil { + return EthBalMonSetWatchListOpOutput{}, + fmt.Errorf("failed to get MCMS address: %w", err) + } + + ethBalMon, err := eth_balance_monitor_wrapper.NewEthBalanceMonitor( + common.HexToAddress(ethBalMonAddr), + chain.Client, + ) + if err != nil { + return EthBalMonSetWatchListOpOutput{}, + fmt.Errorf("failed to instantiate EthBalanceMonitor at %s: %w", ethBalMonAddr, err) + } + + minBalancesWei := make([]*big.Int, len(input.MinBalancesWei)) + for i := range input.MinBalancesWei { + minBalancesWei[i] = &input.MinBalancesWei[i] + } + + topAmountsWei := make([]*big.Int, len(input.TopUpAmountsWei)) + for i := range input.TopUpAmountsWei { + topAmountsWei[i] = &input.TopUpAmountsWei[i] + } + setWatchListTx, err := ethBalMon.SetWatchList(cldf.SimTransactOpts(), input.Addresses, minBalancesWei, topAmountsWei) + if err != nil { + return EthBalMonSetWatchListOpOutput{}, fmt.Errorf("failed to generate setWatchList calldata on chain %d: %w ", input.ChainSelector, err) + } + + batch := mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(input.ChainSelector), + Transactions: []mcmstypes.Transaction{ + { + OperationMetadata: mcmstypes.OperationMetadata{ + ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, + Tags: []string{ + "setWatchList", + }, + }, + To: ethBalMonAddr, + Data: setWatchListTx.Data(), + AdditionalFields: json.RawMessage(`{"value": 0}`), + }, + }, + } + + chainInspector := mcmsevmsdk.NewInspector(chain.Client) + + b.Logger.Infow("Generated EthBalMon setWatchlist batch", + "chainSelector", input.ChainSelector, + "ethBalMon", ethBalMonAddr, + "newWatchList", input.Addresses, + ) + + return EthBalMonSetWatchListOpOutput{ + ChainSelector: input.ChainSelector, + BatchOperation: batch, + TimelockAddress: timelockAddr, + MCMSAddress: mcmsAddr, + Inspector: chainInspector, + }, nil + }, +) diff --git a/deployment/vault/changeset/types/types.go b/deployment/vault/changeset/types/types.go index b1e444ebe4e..d286abc8275 100644 --- a/deployment/vault/changeset/types/types.go +++ b/deployment/vault/changeset/types/types.go @@ -4,6 +4,7 @@ package types import ( "math/big" + "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" ) @@ -101,3 +102,12 @@ type SetKeeperRegistryChainConfig struct { type EthBalMonSetKeeperRegistryAddressInput struct { Chains map[uint64]SetKeeperRegistryChainConfig `json:"chains"` } + +type EthBalMonSetWatchListChainConfig struct { + Addresses []common.Address `json:"addresses"` + MinBalancesWei []big.Int `json:"min_balance_wei"` + TopUpAmountsWei []big.Int `json:"topup_amounts_wei"` +} +type EthBalMonSetWatchListInput struct { + Chains map[uint64]EthBalMonSetWatchListChainConfig `json:"chains"` +} From 45d75693bce7d3d1a889c112e46af5822cf4fa05 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 6 May 2026 18:07:04 -0300 Subject: [PATCH 07/35] fix import error --- deployment/vault/changeset/ethbalmon_setWatchList.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/vault/changeset/ethbalmon_setWatchList.go b/deployment/vault/changeset/ethbalmon_setWatchList.go index 6d537281650..dc214c97003 100644 --- a/deployment/vault/changeset/ethbalmon_setWatchList.go +++ b/deployment/vault/changeset/ethbalmon_setWatchList.go @@ -7,7 +7,6 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" - "github.com/smartco ntractkit/mcms" cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -15,6 +14,7 @@ import ( "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" + "github.com/smartcontractkit/mcms" mcmssdk "github.com/smartcontractkit/mcms/sdk" mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" mcmstypes "github.com/smartcontractkit/mcms/types" From a25ed4b6e61fc3755a0e7d4171935b8a007ab8ed Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 6 May 2026 18:14:31 -0300 Subject: [PATCH 08/35] Change logic on setKeeperRegistryAddress to use new proposal builder --- .../ethbalmon_setKeeperRegistryAddress.go | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go index eef441ea9ba..f5e4aed5805 100644 --- a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go +++ b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go @@ -10,9 +10,12 @@ import ( cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" "github.com/smartcontractkit/mcms" + mcmssdk "github.com/smartcontractkit/mcms/sdk" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" mcmstypes "github.com/smartcontractkit/mcms/types" ) @@ -93,8 +96,9 @@ var SetKeeperRegistrySequence = operations.NewSequence( } var batches []mcmstypes.BatchOperation - timelockAddresses := make(map[mcmstypes.ChainSelector]string) - chainMetadata := make(map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata) + timelockAddresses := make(map[uint64]string) + mcmAddressByChain := make(map[uint64]string) + inspectorPerChain := make(map[uint64]mcmssdk.Inspector) for chainSelector, chainConfig := range input.Chains { opReport, err := operations.ExecuteOperation( @@ -114,21 +118,20 @@ var SetKeeperRegistrySequence = operations.NewSequence( opOut := opReport.Output batches = append(batches, opOut.BatchOperation) - timelockAddresses[mcmstypes.ChainSelector(chainSelector)] = opOut.TimelockAddress - chainMetadata[mcmstypes.ChainSelector(chainSelector)] = mcmstypes.ChainMetadata{ - StartingOpCount: 0, - MCMAddress: opOut.MCMSAddress, - } + timelockAddresses[chainSelector] = opOut.TimelockAddress + mcmAddressByChain[chainSelector] = opOut.MCMSAddress + inspectorPerChain[chainSelector] = opOut.Inspector } - proposal, err := mcms.NewTimelockProposalBuilder(). - SetVersion("v1"). - SetAction(mcmstypes.TimelockActionBypass). - SetTimelockAddresses(timelockAddresses). - SetChainMetadata(chainMetadata). - SetOperations(batches). - SetDescription("Set Keeper Registry address on EthBalanceMonitor across chains"). - Build() + proposal, err := proposalutils.BuildProposalFromBatchesV2( + deps.Environment, + timelockAddresses, + mcmAddressByChain, + inspectorPerChain, + batches, + "EthBalMon SetKeeperRegistryAddress", + proposalutils.TimelockConfig{MinDelay: 0}, + ) if err != nil { return EthBalMonSetKeeperRegistryAddressSequenceOutput{}, fmt.Errorf("failed to build timelock proposal: %w", err) @@ -155,6 +158,7 @@ type SetKeeperRegistryOperationOutput struct { BatchOperation mcmstypes.BatchOperation `json:"batch_operation"` TimelockAddress string `json:"timelock_address"` MCMSAddress string `json:"mcms_address"` + Inspector *mcmsevmsdk.Inspector `json:"inspector"` } var SetKeeperRegistryOperation = operations.NewOperation( @@ -236,6 +240,8 @@ var SetKeeperRegistryOperation = operations.NewOperation( }, } + chainInspector := mcmsevmsdk.NewInspector(chain.Client) + b.Logger.Infow("Generated EthBalMon set keeper registry batch", "chainSelector", input.ChainSelector, "ethBalMon", ethBalMonAddr, @@ -247,6 +253,7 @@ var SetKeeperRegistryOperation = operations.NewOperation( BatchOperation: batch, TimelockAddress: timelockAddr, MCMSAddress: mcmsAddr, + Inspector: chainInspector, }, nil }, ) From 4610db7f6f984776f7482b97e0de29cf7d8349c2 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Thu, 7 May 2026 16:30:31 -0300 Subject: [PATCH 09/35] Add withdraw changeset --- .../vault/changeset/ethbalmon_withdraw.go | 217 ++++++++++++++++++ deployment/vault/changeset/types/types.go | 11 + deployment/vault/changeset/validation.go | 20 ++ 3 files changed, 248 insertions(+) create mode 100644 deployment/vault/changeset/ethbalmon_withdraw.go diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go new file mode 100644 index 00000000000..6d2ff74ba5f --- /dev/null +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -0,0 +1,217 @@ +package changeset + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/common" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" + vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" + "github.com/smartcontractkit/mcms" + mcmssdk "github.com/smartcontractkit/mcms/sdk" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +type ethBalMonWithdraw struct{} + +var EthBalMonWithdraw cldf.ChangeSetV2[vaulttypes.EthBalMonWithdrawInput] = ethBalMonWithdraw{} + +func (w ethBalMonWithdraw) VerifyPreconditions(env cldf.Environment, config vaulttypes.EthBalMonWithdrawInput) error { + return ValidateEthBalMonWithdrawConfig(env.GetContext(), env, config) +} + +func (w ethBalMonWithdraw) Apply(e cldf.Environment, config vaulttypes.EthBalMonWithdrawInput) (cldf.ChangesetOutput, error) { + logger := e.Logger + logger.Infow("Generating EthBalMon withdraw proposal", "numChains", len(config.Chains)) + + evmChains := e.BlockChains.EVMChains() + + var primaryChain cldf_evm.Chain + for chainSelector := range config.Chains { + primaryChain = evmChains[chainSelector] + break + } + + deps := VaultDeps{ + Auth: primaryChain.DeployerKey, + Chain: primaryChain, + Environment: e, + DataStore: e.DataStore, + } + seqInput := EthBalMonWithdrawSeqInput{ + Chains: config.Chains, + } + seqReport, err := operations.ExecuteSequence(e.OperationsBundle, EthBalMonWithdrawSequence, deps, seqInput) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed on EthBalMonWithdrawSequence sequence: %w", err) + } + + return cldf.ChangesetOutput{ + MCMSTimelockProposals: seqReport.Output.MCMSTimelockProposals, + }, nil +} + +type EthBalMonWithdrawSeqInput struct { + Chains map[uint64]vaulttypes.EthBalMonWithdrawChainConfig `json:"chains"` +} + +type EthBalMonWithdrawSeqOutput struct { + MCMSTimelockProposals []mcms.TimelockProposal `json:"mcms_timelock_proposals"` +} + +var EthBalMonWithdrawSequence = operations.NewSequence( + "ethbalmon-withdraw-sequence", + semver.MustParse("1.0.0"), + "Sequence to create operation for EthBalMon withdraw", + func(b operations.Bundle, deps VaultDeps, input EthBalMonWithdrawSeqInput) (EthBalMonWithdrawSeqOutput, error) { + b.Logger.Infow("Starting EthBalMon withdraw sequence", + "chains", len(input.Chains), + ) + var batches []mcmstypes.BatchOperation + timelockAddresses := make(map[uint64]string) + mcmAddressByChain := make(map[uint64]string) + inspectorPerChain := make(map[uint64]mcmssdk.Inspector) + for chainSelector, chainConfig := range input.Chains { + opReport, err := operations.ExecuteOperation(b, EthBalMonWithdrawOperation, deps, EthBalMonWithdrawOpInput{ + ChainSelector: chainSelector, + Amount: chainConfig.Amount, + Payeer: chainConfig.Payeer, + }) + opOutput := opReport.Output + if err != nil { + return EthBalMonWithdrawSeqOutput{}, fmt.Errorf("chain %d: failed to generate withdraw batch: %w", chainSelector, err) + } + batches = append(batches, opOutput.BatchOperation) + timelockAddresses[chainSelector] = opOutput.TimelockAddress + mcmAddressByChain[chainSelector] = opOutput.MCMSAddress + inspectorPerChain[chainSelector] = opOutput.Inspector + } + + proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, inspectorPerChain, batches, "EthBalMon Withdraw", proposalutils.TimelockConfig{ + MinDelay: 0, + }) + + if err != nil { + return EthBalMonWithdrawSeqOutput{}, fmt.Errorf("failed to build timelock proposal: %w", err) + } + b.Logger.Infow("Generated EthBalMon withdraw proposal", + "chains", len(input.Chains), "operations", len(batches)) + + return EthBalMonWithdrawSeqOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil + }, +) + +type EthBalMonWithdrawOpInput struct { + ChainSelector uint64 `json:"chain_selector"` + Amount uint64 `json:"amount"` + Payeer string `json:"payeer"` +} + +type EthBalMonWithdrawOpOutput struct { + ChainSelector uint64 `json:"chain_selector"` + BatchOperation mcmstypes.BatchOperation `json:"batch_operation"` + TimelockAddress string `json:"timelock_address"` + MCMSAddress string `json:"mcms_address"` + Inspector *mcmsevmsdk.Inspector `json:"inspector"` +} + +var EthBalMonWithdrawOperation = operations.NewOperation( + "ethbalmon-withdraw-operation", + semver.MustParse("1.0.0"), + "", + func(b operations.Bundle, deps VaultDeps, input EthBalMonWithdrawOpInput) (EthBalMonWithdrawOpOutput, error) { + b.Logger.Infow("Starting EthBalMon withdraw operation", + "chainsel", input.ChainSelector, + ) + + chain, ok := deps.Environment.BlockChains.EVMChains()[input.ChainSelector] + + if !ok { + return EthBalMonWithdrawOpOutput{}, fmt.Errorf("chain not found in environment: %d", input.ChainSelector) + } + + ethBalMonAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + cldf.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), + ) + if err != nil { + return EthBalMonWithdrawOpOutput{}, + fmt.Errorf("failed to get EthBalMon address: %w", err) + } + + timelockAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + commontypes.RBACTimelock, + ) + if err != nil { + return EthBalMonWithdrawOpOutput{}, + fmt.Errorf("failed to get timelock address: %w", err) + } + mcmsAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + commontypes.ManyChainMultisig, + ) + if err != nil { + return EthBalMonWithdrawOpOutput{}, + fmt.Errorf("failed to get MCMS address: %w", err) + } + + ethBalMon, err := eth_balance_monitor_wrapper.NewEthBalanceMonitor(common.HexToAddress(ethBalMonAddr), chain.Client) + if err != nil { + return EthBalMonWithdrawOpOutput{}, + fmt.Errorf("failed to instantiate EthBalanceMonitor at %s: %w", ethBalMonAddr, err) + } + + amountBigInt := big.NewInt(int64(input.Amount)) + withdrawTx, err := ethBalMon.Withdraw(cldf.SimTransactOpts(), amountBigInt, common.HexToAddress(input.Payeer)) + if err != nil { + return EthBalMonWithdrawOpOutput{}, fmt.Errorf("failed to generate withdraw calldata on chain %d: %w ", input.ChainSelector, err) + } + batch := mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(input.ChainSelector), + Transactions: []mcmstypes.Transaction{ + { + OperationMetadata: mcmstypes.OperationMetadata{ + ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, + Tags: []string{ + "withdraw", + }, + }, + To: ethBalMonAddr, + Data: withdrawTx.Data(), + AdditionalFields: json.RawMessage(`{"value": 0}`), + }, + }, + } + + chainInspector := mcmsevmsdk.NewInspector(chain.Client) + + b.Logger.Infow("Generated EthBalMon withdraw batch", + "chainSelector", input.ChainSelector, + "ethBalMon", ethBalMonAddr, + "amount", input.Amount, + "payeer", input.Payeer, + ) + + return EthBalMonWithdrawOpOutput{ + ChainSelector: input.ChainSelector, + BatchOperation: batch, + TimelockAddress: timelockAddr, + MCMSAddress: mcmsAddr, + Inspector: chainInspector, + }, nil + }, +) diff --git a/deployment/vault/changeset/types/types.go b/deployment/vault/changeset/types/types.go index d286abc8275..182402450e5 100644 --- a/deployment/vault/changeset/types/types.go +++ b/deployment/vault/changeset/types/types.go @@ -103,6 +103,7 @@ type EthBalMonSetKeeperRegistryAddressInput struct { Chains map[uint64]SetKeeperRegistryChainConfig `json:"chains"` } +// setWatchList config type EthBalMonSetWatchListChainConfig struct { Addresses []common.Address `json:"addresses"` MinBalancesWei []big.Int `json:"min_balance_wei"` @@ -111,3 +112,13 @@ type EthBalMonSetWatchListChainConfig struct { type EthBalMonSetWatchListInput struct { Chains map[uint64]EthBalMonSetWatchListChainConfig `json:"chains"` } + +// withdraw config +type EthBalMonWithdrawChainConfig struct { + Amount uint64 `json:"amount"` + Payeer string `json:"payeer"` +} + +type EthBalMonWithdrawInput struct { + Chains map[uint64]EthBalMonWithdrawChainConfig `json:"chains"` +} diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index 28ca0096478..3c8cbc1431a 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -269,3 +269,23 @@ func ValidateSetKeeperRegistryAddressConfig(ctx context.Context, env cldf.Enviro return nil } + +func ValidateEthBalMonWithdrawConfig(ctx context.Context, env cldf.Environment, cfg types.EthBalMonWithdrawInput) error { + if len(cfg.Chains) == 0 { + return fmt.Errorf("no chains provided") + } + + for chainSelector, chainConfig := range cfg.Chains { + if _, ok := env.BlockChains.EVMChains()[chainSelector]; !ok { + return fmt.Errorf("chain not found in environment: %d", chainSelector) + } + if chainConfig.Amount == 0 { + return fmt.Errorf("chain %d: amount to withdraw cannot be 0 (zero)", chainSelector) + } + if !common.IsHexAddress(chainConfig.Payeer) { + return fmt.Errorf("chain %d: invalid payeer address: %s", chainSelector, chainConfig.Payeer) + } + } + + return nil +} From 57bde8318dfbec4e9fd3943bdf142206144ad8b1 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Fri, 8 May 2026 09:35:32 -0300 Subject: [PATCH 10/35] add transferOwnership changeset --- .../changeset/ethbalmon_transferOwnership.go | 212 ++++++++++++++++++ .../vault/changeset/ethbalmon_withdraw.go | 2 +- deployment/vault/changeset/types/types.go | 9 + deployment/vault/changeset/validation.go | 23 ++ 4 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 deployment/vault/changeset/ethbalmon_transferOwnership.go diff --git a/deployment/vault/changeset/ethbalmon_transferOwnership.go b/deployment/vault/changeset/ethbalmon_transferOwnership.go new file mode 100644 index 00000000000..ee0dead19cb --- /dev/null +++ b/deployment/vault/changeset/ethbalmon_transferOwnership.go @@ -0,0 +1,212 @@ +package changeset + +import ( + "encoding/json" + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/common" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" + vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" + "github.com/smartcontractkit/mcms" + mcmssdk "github.com/smartcontractkit/mcms/sdk" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +type ethBalMonTransferOwnership struct{} + +var EthBalMonTransferOwnership cldf.ChangeSetV2[vaulttypes.EthBalMonTransferOwnershipInput] = ethBalMonTransferOwnership{} + +func (tw ethBalMonTransferOwnership) VerifyPreconditions(env cldf.Environment, config vaulttypes.EthBalMonTransferOwnershipInput) error { + return ValidateEthBalMonTransferOwnershipConfig(env.GetContext(), env, config) +} + +func (tw ethBalMonTransferOwnership) Apply(e cldf.Environment, config vaulttypes.EthBalMonTransferOwnershipInput) (cldf.ChangesetOutput, error) { + logger := e.Logger + logger.Infow("Generating EthBalMon transferOwnership proposal", "numChains", len(config.Chains)) + + evmChains := e.BlockChains.EVMChains() + + var primaryChain cldf_evm.Chain + for chainSelector := range config.Chains { + primaryChain = evmChains[chainSelector] + break + } + + deps := VaultDeps{ + Auth: primaryChain.DeployerKey, + Chain: primaryChain, + Environment: e, + DataStore: e.DataStore, + } + seqInput := EthBalMonTransferOwnershipSeqInput{ + Chains: config.Chains, + } + seqReport, err := operations.ExecuteSequence(e.OperationsBundle, EthBalMonTransferOwnershipSequence, deps, seqInput) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed on EthBalMonTransferOwnershipSequence sequence: %w", err) + } + + return cldf.ChangesetOutput{ + MCMSTimelockProposals: seqReport.Output.MCMSTimelockProposals, + }, nil +} + +type EthBalMonTransferOwnershipSeqInput struct { + Chains map[uint64]vaulttypes.EthBalMonTransferOwnershipChainConfig `json:"chains"` +} + +type EthBalMonTransferOwnershipSeqOutput struct { + MCMSTimelockProposals []mcms.TimelockProposal `json:"mcms_timelock_proposals"` +} + +var EthBalMonTransferOwnershipSequence = operations.NewSequence( + "ethbalmon-transferownership-operation", + semver.MustParse("1.0.0"), + "Sequence to create transferOwnership EthBalMon batch transaction", + func(b operations.Bundle, deps VaultDeps, input EthBalMonTransferOwnershipSeqInput) (EthBalMonTransferOwnershipSeqOutput, error) { + b.Logger.Infow("Starting EthBalMon transferOwnership sequence", + "chains", len(input.Chains), + ) + var batches []mcmstypes.BatchOperation + timelockAddresses := make(map[uint64]string) + mcmAddressByChain := make(map[uint64]string) + inspectorPerChain := make(map[uint64]mcmssdk.Inspector) + for chainSelector, chainConfig := range input.Chains { + opReport, err := operations.ExecuteOperation(b, EthBalMonTransferOwnershipOperation, deps, EthBalMonTransferOwnershipOpInput{ + ChainSelector: chainSelector, + NewOwner: chainConfig.NewOwner, + }) + opOutput := opReport.Output + if err != nil { + return EthBalMonTransferOwnershipSeqOutput{}, fmt.Errorf("chain %d: failed to generate ownership batch: %w", chainSelector, err) + } + batches = append(batches, opOutput.BatchOperation) + timelockAddresses[chainSelector] = opOutput.TimelockAddress + mcmAddressByChain[chainSelector] = opOutput.MCMSAddress + inspectorPerChain[chainSelector] = opOutput.Inspector + } + + proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, inspectorPerChain, batches, "EthBalMon transferOwnership", proposalutils.TimelockConfig{ + MinDelay: 0, + }) + + if err != nil { + return EthBalMonTransferOwnershipSeqOutput{}, fmt.Errorf("failed to build timelock proposal: %w", err) + } + b.Logger.Infow("Generated EthBalMon transferOwnership proposal", + "chains", len(input.Chains), "operations", len(batches)) + + return EthBalMonTransferOwnershipSeqOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + }, nil + }, +) + +type EthBalMonTransferOwnershipOpInput struct { + ChainSelector uint64 `json:"chain_selector"` + NewOwner string `json:"new_owner"` +} + +type EthBalMonTransferOwnershipOpOutput struct { + ChainSelector uint64 `json:"chain_selector"` + BatchOperation mcmstypes.BatchOperation `json:"batch_operation"` + TimelockAddress string `json:"timelock_address"` + MCMSAddress string `json:"mcms_address"` + Inspector *mcmsevmsdk.Inspector `json:"inspector"` +} + +var EthBalMonTransferOwnershipOperation = operations.NewOperation( + "ethbalmon-transferownership-operation", + semver.MustParse("1.0.0"), + "Operation to create transferOwnership EthBalMon batch transaction", + func(b operations.Bundle, deps VaultDeps, input EthBalMonTransferOwnershipOpInput) (EthBalMonTransferOwnershipOpOutput, error) { + b.Logger.Infow("Starting EthBalMon transferOwnership operation", + "chainsel", input.ChainSelector, + ) + + chain, ok := deps.Environment.BlockChains.EVMChains()[input.ChainSelector] + + if !ok { + return EthBalMonTransferOwnershipOpOutput{}, fmt.Errorf("chain not found in environment: %d", input.ChainSelector) + } + + ethBalMonAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + cldf.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), + ) + if err != nil { + return EthBalMonTransferOwnershipOpOutput{}, + fmt.Errorf("failed to get EthBalMon address: %w", err) + } + + timelockAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + commontypes.RBACTimelock, + ) + if err != nil { + return EthBalMonTransferOwnershipOpOutput{}, + fmt.Errorf("failed to get timelock address: %w", err) + } + mcmsAddr, err := mustGetContractAddress( + deps.DataStore, + input.ChainSelector, + commontypes.ManyChainMultisig, + ) + if err != nil { + return EthBalMonTransferOwnershipOpOutput{}, + fmt.Errorf("failed to get MCMS address: %w", err) + } + + ethBalMon, err := eth_balance_monitor_wrapper.NewEthBalanceMonitor(common.HexToAddress(ethBalMonAddr), chain.Client) + if err != nil { + return EthBalMonTransferOwnershipOpOutput{}, + fmt.Errorf("failed to instantiate EthBalanceMonitor at %s: %w", ethBalMonAddr, err) + } + + transferOwnershipTx, err := ethBalMon.TransferOwnership(cldf.SimTransactOpts(), common.HexToAddress(input.NewOwner)) + if err != nil { + return EthBalMonTransferOwnershipOpOutput{}, fmt.Errorf("failed to generate transferOwnership calldata on chain %d: %w ", input.ChainSelector, err) + } + batch := mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(input.ChainSelector), + Transactions: []mcmstypes.Transaction{ + { + OperationMetadata: mcmstypes.OperationMetadata{ + ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, + Tags: []string{ + "transferOwnership", + }, + }, + To: ethBalMonAddr, + Data: transferOwnershipTx.Data(), + AdditionalFields: json.RawMessage(`{"value": 0}`), + }, + }, + } + + chainInspector := mcmsevmsdk.NewInspector(chain.Client) + + b.Logger.Infow("Generated EthBalMon transferOwnership batch", + "chainSelector", input.ChainSelector, + "ethBalMon", ethBalMonAddr, + "newOwner", input.NewOwner, + ) + + return EthBalMonTransferOwnershipOpOutput{ + ChainSelector: input.ChainSelector, + BatchOperation: batch, + TimelockAddress: timelockAddr, + MCMSAddress: mcmsAddr, + Inspector: chainInspector, + }, nil + }, +) diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go index 6d2ff74ba5f..1a4c8d85517 100644 --- a/deployment/vault/changeset/ethbalmon_withdraw.go +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -128,7 +128,7 @@ type EthBalMonWithdrawOpOutput struct { var EthBalMonWithdrawOperation = operations.NewOperation( "ethbalmon-withdraw-operation", semver.MustParse("1.0.0"), - "", + "Operation to create withdraw EthBalMon batch transaction", func(b operations.Bundle, deps VaultDeps, input EthBalMonWithdrawOpInput) (EthBalMonWithdrawOpOutput, error) { b.Logger.Infow("Starting EthBalMon withdraw operation", "chainsel", input.ChainSelector, diff --git a/deployment/vault/changeset/types/types.go b/deployment/vault/changeset/types/types.go index 182402450e5..9dca1440cd2 100644 --- a/deployment/vault/changeset/types/types.go +++ b/deployment/vault/changeset/types/types.go @@ -122,3 +122,12 @@ type EthBalMonWithdrawChainConfig struct { type EthBalMonWithdrawInput struct { Chains map[uint64]EthBalMonWithdrawChainConfig `json:"chains"` } + +// transferOwnership config +type EthBalMonTransferOwnershipChainConfig struct { + NewOwner string `json:"new_owner"` +} + +type EthBalMonTransferOwnershipInput struct { + Chains map[uint64]EthBalMonTransferOwnershipChainConfig `json:"chains"` +} diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index 3c8cbc1431a..36653188751 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -285,6 +285,29 @@ func ValidateEthBalMonWithdrawConfig(ctx context.Context, env cldf.Environment, if !common.IsHexAddress(chainConfig.Payeer) { return fmt.Errorf("chain %d: invalid payeer address: %s", chainSelector, chainConfig.Payeer) } + if chainConfig.Payeer == "" || chainConfig.Payeer == "0x0000000000000000000000000000000000000000" { + return fmt.Errorf("chain %d: payeer address cannot be zero address", chainSelector) + } + } + + return nil +} + +func ValidateEthBalMonTransferOwnershipConfig(ctx context.Context, env cldf.Environment, cfg types.EthBalMonTransferOwnershipInput) error { + if len(cfg.Chains) == 0 { + return fmt.Errorf("no chains provided") + } + + for chainSelector, chainConfig := range cfg.Chains { + if _, ok := env.BlockChains.EVMChains()[chainSelector]; !ok { + return fmt.Errorf("chain not found in environment: %d", chainSelector) + } + if !common.IsHexAddress(chainConfig.NewOwner) { + return fmt.Errorf("chain %d: invalid payeer address: %s", chainSelector, chainConfig.NewOwner) + } + if chainConfig.NewOwner == "" || chainConfig.NewOwner == "0x0000000000000000000000000000000000000000" { + return fmt.Errorf("chain %d: payeer address cannot be zero address", chainSelector) + } } return nil From f4116b5fbf826e4b9956ed722d0be96972a734a6 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Fri, 8 May 2026 14:43:00 -0300 Subject: [PATCH 11/35] Add deploy ethbalmon testing --- .../vault/changeset/ethbalmon_deploy.go | 63 ++-- .../vault/changeset/ethbalmon_deploy_test.go | 297 +++++++++++++++++- .../ethbalmon_setKeeperRegistryAddress.go | 2 +- .../vault/changeset/ethbalmon_setWatchList.go | 2 +- .../changeset/ethbalmon_transferOwnership.go | 2 +- .../vault/changeset/ethbalmon_withdraw.go | 2 +- 6 files changed, 338 insertions(+), 30 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy.go b/deployment/vault/changeset/ethbalmon_deploy.go index af2ea6206ca..c73d2ece68b 100644 --- a/deployment/vault/changeset/ethbalmon_deploy.go +++ b/deployment/vault/changeset/ethbalmon_deploy.go @@ -12,9 +12,12 @@ import ( cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" "github.com/smartcontractkit/mcms" + mcmssdk "github.com/smartcontractkit/mcms/sdk" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" ds "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" mcmstypes "github.com/smartcontractkit/mcms/types" @@ -187,12 +190,16 @@ var DeployEthBalMonSequence = operations.NewSequence( if !ok { return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain not found in environment: %d", chainSelector) } - minWait := effectiveMinWaitPeriodSeconds(*chainConfig.SetMinWaitPeriodSeconds) + var rawMinWait uint64 + if chainConfig.SetMinWaitPeriodSeconds != nil { + rawMinWait = *chainConfig.SetMinWaitPeriodSeconds + } + minWait := effectiveMinWaitPeriodSeconds(rawMinWait) timelockAddr, err := mustGetContractAddress(deps.DataStore, chainSelector, commontypes.RBACTimelock) if err != nil { return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain %d: failed to get timelock address: %w", chainSelector, err) } - mcmsAddr, err := mustGetContractAddress(deps.DataStore, chainSelector, commontypes.ManyChainMultisig) + mcmsAddr, err := mustGetContractAddress(deps.DataStore, chainSelector, commontypes.BypasserManyChainMultisig) if err != nil { return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain %d: failed to get mcms address: %w", chainSelector, err) } @@ -411,8 +418,9 @@ func BuildAcceptOwnershipTimelockProposal( } var batches []mcmstypes.BatchOperation - timelockAddresses := make(map[mcmstypes.ChainSelector]string) - chainMetadata := make(map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata) + timelockAddresses := make(map[uint64]string) + mcmAddressByChain := make(map[uint64]string) + inspectorPerChain := make(map[uint64]mcmssdk.Inspector) for chainSelector, contractAddr := range input.ContractsByChain { chain, ok := e.BlockChains.EVMChains()[chainSelector] @@ -429,11 +437,20 @@ func BuildAcceptOwnershipTimelockProposal( return nil, fmt.Errorf("chain %d: %w", chainSelector, err) } - mcmsAddr, err := mustGetContractAddress( - e.DataStore, - chainSelector, - commontypes.ManyChainMultisig, - ) + var mcmsAddr string + if input.Action == mcmstypes.TimelockActionBypass { + mcmsAddr, err = mustGetContractAddress( + e.DataStore, + chainSelector, + commontypes.BypasserManyChainMultisig, + ) + } else { + mcmsAddr, err = mustGetContractAddress( + e.DataStore, + chainSelector, + commontypes.ProposerManyChainMultisig, + ) + } if err != nil { return nil, fmt.Errorf("chain %d: %w", chainSelector, err) } @@ -466,11 +483,9 @@ func BuildAcceptOwnershipTimelockProposal( }, }) - timelockAddresses[mcmstypes.ChainSelector(chainSelector)] = timelockAddr - chainMetadata[mcmstypes.ChainSelector(chainSelector)] = mcmstypes.ChainMetadata{ - StartingOpCount: 0, - MCMAddress: mcmsAddr, - } + timelockAddresses[chainSelector] = timelockAddr + mcmAddressByChain[chainSelector] = mcmsAddr + inspectorPerChain[chainSelector] = mcmsevmsdk.NewInspector(chain.Client) } description := input.Description @@ -478,15 +493,17 @@ func BuildAcceptOwnershipTimelockProposal( description = "Accept ownership of EthBalanceMonitor across chains" } - proposal, err := mcms.NewTimelockProposalBuilder(). - SetVersion("v1"). - SetAction(input.Action). - SetTimelockAddresses(timelockAddresses). - SetChainMetadata(chainMetadata). - SetOperations(batches). - SetDescription(description). - Build() - + proposal, err := proposalutils.BuildProposalFromBatchesV2( + e, + timelockAddresses, + mcmAddressByChain, + inspectorPerChain, + batches, + description, + proposalutils.TimelockConfig{ + MinDelay: 0, + }, + ) if err != nil { return nil, fmt.Errorf("failed to build timelock proposal: %w", err) } diff --git a/deployment/vault/changeset/ethbalmon_deploy_test.go b/deployment/vault/changeset/ethbalmon_deploy_test.go index fe113ab17fd..c354a26d244 100644 --- a/deployment/vault/changeset/ethbalmon_deploy_test.go +++ b/deployment/vault/changeset/ethbalmon_deploy_test.go @@ -1,20 +1,32 @@ package changeset import ( + "encoding/json" "fmt" "math" + "strconv" "testing" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" + mcmstypes "github.com/smartcontractkit/mcms/types" + + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" - "github.com/stretchr/testify/require" ) func TestDeployEthBalMonValidation(t *testing.T) { t.Parallel() selector := chainselectors.TEST_90000001.Selector + selectorOther := chainselectors.TEST_90000002.Selector env, err := environment.New(t.Context(), environment.WithEVMSimulated(t, []uint64{selector}), @@ -47,6 +59,18 @@ func TestDeployEthBalMonValidation(t *testing.T) { wantError: true, errorMsg: fmt.Sprintf("unknown chain selector %d", uint64(math.MaxUint64)), }, + { + name: "chain not in environment", + config: types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selectorOther: { + SetKeeperRegistryAddress: "0x1234567890123456789012345678901234567890", + }, + }, + }, + wantError: true, + errorMsg: "not found in environment", + }, { name: "empty setKeeperRegistryAddress", config: types.DeployEthBalMonInput{ @@ -102,13 +126,280 @@ func TestDeployEthBalMonValidation(t *testing.T) { } } +func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { + t.Parallel() + + t.Run("rejects empty contract set", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + env, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, []uint64{selector}), + ) + require.NoError(t, err) + + _, err = BuildAcceptOwnershipTimelockProposal(*env, AcceptOwnershipProposalInput{ + ContractsByChain: map[uint64]string{}, + Description: "test", + Action: mcmstypes.TimelockActionBypass, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "no contracts provided") + }) +} + func TestDeployEthBalMonChangeset(t *testing.T) { t.Parallel() - // rt, err := runtime.New(t.Context(), runtime.WithEnvOpts()) - // require.NoError(t, err) t.Run("single chain", func(t *testing.T) { t.Parallel() + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + customWait := uint64(120) + cfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: { + SetKeeperRegistryAddress: testAddr1, + SetMinWaitPeriodSeconds: &customWait, + }, + }, + } + + require.NoError(t, DeployEthBalMonChangeSet.VerifyPreconditions(rt.Environment(), cfg)) + + out, err := DeployEthBalMonChangeSet.Apply(rt.Environment(), cfg) + require.NoError(t, err) + assertEthBalMonDeployOutput(t, rt.Environment(), out, cfg) + }) + + t.Run("default min wait when unset uses 60s", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + cfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: { + SetKeeperRegistryAddress: testAddr1, + }, + }, + } + + out, err := DeployEthBalMonChangeSet.Apply(rt.Environment(), cfg) + require.NoError(t, err) + + mds, err := out.DataStore.ContractMetadata().Fetch() + require.NoError(t, err) + require.Len(t, mds, 1) + mdMap := contractMetadataMap(t, mds[0].Metadata) + require.Equal(t, uint64(60), uint64FromAny(t, mdMap["minWaitPeriodSeconds"])) + }) + + t.Run("multiple chains", func(t *testing.T) { + t.Parallel() + + selector1 := chainselectors.TEST_90000001.Selector + selector2 := chainselectors.TEST_90000002.Selector + selectors := []uint64{selector1, selector2} + + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, selectors), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, selectors) + fundDeployerAccounts(t, rt.Environment(), selectors) + + cfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector1: {SetKeeperRegistryAddress: testAddr1}, + selector2: {SetKeeperRegistryAddress: testAddr2}, + }, + } + + out, err := DeployEthBalMonChangeSet.Apply(rt.Environment(), cfg) + require.NoError(t, err) + assertEthBalMonDeployOutput(t, rt.Environment(), out, cfg) + }) + + t.Run("verify preconditions rejects invalid config", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + err = DeployEthBalMonChangeSet.VerifyPreconditions(rt.Environment(), types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{}, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "chains must not be empty") }) + + t.Run("apply without MCMS infrastructure fails", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + cfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: {SetKeeperRegistryAddress: testAddr1}, + }, + } + + _, err = DeployEthBalMonChangeSet.Apply(rt.Environment(), cfg) + require.Error(t, err) + require.ErrorContains(t, err, "timelock") + }) +} + +// assertEthBalMonDeployOutput checks datastore, on-chain owner (post transferOwnership, pre-accept), +// and the accept-ownership timelock proposal. +func assertEthBalMonDeployOutput( + t *testing.T, + env cldf.Environment, + out cldf.ChangesetOutput, + cfg types.DeployEthBalMonInput, +) { + t.Helper() + + n := len(cfg.Chains) + require.NotNil(t, out.DataStore) + + addrs, err := out.DataStore.Addresses().Fetch() + require.NoError(t, err) + require.Len(t, addrs, n) + + mds, err := out.DataStore.ContractMetadata().Fetch() + require.NoError(t, err) + require.Len(t, mds, n) + + bySel := make(map[uint64]datastore.ContractMetadata) + for _, m := range mds { + bySel[m.ChainSelector] = m + } + + for sel, chainCfg := range cfg.Chains { + timelockAddr, err := GetContractAddress(env.DataStore, sel, commontypes.RBACTimelock) + require.NoError(t, err) + mcmsAddr, err := GetContractAddress(env.DataStore, sel, commontypes.BypasserManyChainMultisig) + require.NoError(t, err) + + meta, ok := bySel[sel] + require.True(t, ok, "missing contract metadata for chain %d", sel) + require.NotEmpty(t, meta.Address) + + md := contractMetadataMap(t, meta.Metadata) + require.Equal(t, chainCfg.SetKeeperRegistryAddress, md["keeperRegistryAddress"]) + + minWait := chainCfgMinWaitForEffective(chainCfg) + require.Equal(t, effectiveMinWaitPeriodSeconds(minWait), uint64FromAny(t, md["minWaitPeriodSeconds"])) + + require.NotEmpty(t, md["deployTxHash"]) + require.NotZero(t, uint64FromAny(t, md["deployBlockNumber"])) + require.Equal(t, timelockAddr, md["timelockAddress"]) + require.Equal(t, mcmsAddr, md["mcmsAddress"]) + require.NotEmpty(t, md["transferOwnershipTxHash"]) + + ebmAddr, err := GetContractAddress(out.DataStore, sel, cldf.ContractType(types.ETHBALMON_CONTRACT_TYPE)) + require.NoError(t, err) + + chain := env.BlockChains.EVMChains()[sel] + c, err := eth_balance_monitor_wrapper.NewEthBalanceMonitor(common.HexToAddress(ebmAddr), chain.Client) + require.NoError(t, err) + owner, err := c.Owner(nil) + require.NoError(t, err) + require.Equal(t, chain.DeployerKey.From, owner, + "ConfirmedOwner keeps owner until timelock calls acceptOwnership") + } + + require.Len(t, out.MCMSTimelockProposals, 1) + prop := out.MCMSTimelockProposals[0] + require.Contains(t, prop.Description, "EthBalanceMonitor") + require.Len(t, prop.Operations, n) + + seen := make(map[uint64]bool) + for _, op := range prop.Operations { + sel := uint64(op.ChainSelector) + seen[sel] = true + require.Len(t, op.Transactions, 1) + tx := op.Transactions[0] + require.Equal(t, types.ETHBALMON_CONTRACT_TYPE, tx.OperationMetadata.ContractType) + require.Contains(t, tx.OperationMetadata.Tags, "acceptOwnership") + + wantContract, err := GetContractAddress(out.DataStore, sel, cldf.ContractType(types.ETHBALMON_CONTRACT_TYPE)) + require.NoError(t, err) + require.Equal(t, common.HexToAddress(wantContract), common.HexToAddress(tx.To)) + } + + for sel := range cfg.Chains { + require.True(t, seen[sel], "proposal missing operation for chain %d", sel) + } +} + +func chainCfgMinWaitForEffective(c types.DeployEthBalMonChainConfig) uint64 { + if c.SetMinWaitPeriodSeconds == nil { + return 0 + } + return *c.SetMinWaitPeriodSeconds +} + +func contractMetadataMap(t *testing.T, raw any) map[string]any { + t.Helper() + m, ok := raw.(map[string]any) + require.True(t, ok, "expected metadata map[string]any, got %T", raw) + return m +} + +func uint64FromAny(t *testing.T, v any) uint64 { + t.Helper() + require.NotNil(t, v) + switch x := v.(type) { + case uint64: + return x + case uint: + return uint64(x) + case uint32: + return uint64(x) + case int: + return uint64(x) + case int64: + return uint64(x) + case float64: + return uint64(x) + case json.Number: + i, err := x.Int64() + require.NoError(t, err) + return uint64(i) + case string: + u, err := strconv.ParseUint(x, 10, 64) + require.NoError(t, err) + return u + default: + require.Failf(t, "unexpected type for uint64 metadata field", "%T %#v", v, v) + return 0 + } } diff --git a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go index f5e4aed5805..6838855f9ef 100644 --- a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go +++ b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go @@ -198,7 +198,7 @@ var SetKeeperRegistryOperation = operations.NewOperation( mcmsAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - commontypes.ManyChainMultisig, + commontypes.BypasserManyChainMultisig, ) if err != nil { return SetKeeperRegistryOperationOutput{}, diff --git a/deployment/vault/changeset/ethbalmon_setWatchList.go b/deployment/vault/changeset/ethbalmon_setWatchList.go index dc214c97003..cbc7d6a6c10 100644 --- a/deployment/vault/changeset/ethbalmon_setWatchList.go +++ b/deployment/vault/changeset/ethbalmon_setWatchList.go @@ -168,7 +168,7 @@ var EthBalMonSetWatchListOperation = operations.NewOperation( mcmsAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - commontypes.ManyChainMultisig, + commontypes.BypasserManyChainMultisig, ) if err != nil { return EthBalMonSetWatchListOpOutput{}, diff --git a/deployment/vault/changeset/ethbalmon_transferOwnership.go b/deployment/vault/changeset/ethbalmon_transferOwnership.go index ee0dead19cb..c3fdb965bee 100644 --- a/deployment/vault/changeset/ethbalmon_transferOwnership.go +++ b/deployment/vault/changeset/ethbalmon_transferOwnership.go @@ -159,7 +159,7 @@ var EthBalMonTransferOwnershipOperation = operations.NewOperation( mcmsAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - commontypes.ManyChainMultisig, + commontypes.BypasserManyChainMultisig, ) if err != nil { return EthBalMonTransferOwnershipOpOutput{}, diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go index 1a4c8d85517..13c9000436c 100644 --- a/deployment/vault/changeset/ethbalmon_withdraw.go +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -162,7 +162,7 @@ var EthBalMonWithdrawOperation = operations.NewOperation( mcmsAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - commontypes.ManyChainMultisig, + commontypes.BypasserManyChainMultisig, ) if err != nil { return EthBalMonWithdrawOpOutput{}, From 4bb993af50e0080eb4d4f86c7a2e8882d9c2b4ba Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Fri, 8 May 2026 15:34:17 -0300 Subject: [PATCH 12/35] Add more deploy tests --- .../vault/changeset/ethbalmon_deploy_test.go | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) diff --git a/deployment/vault/changeset/ethbalmon_deploy_test.go b/deployment/vault/changeset/ethbalmon_deploy_test.go index c354a26d244..d635a86351f 100644 --- a/deployment/vault/changeset/ethbalmon_deploy_test.go +++ b/deployment/vault/changeset/ethbalmon_deploy_test.go @@ -146,6 +146,117 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "no contracts provided") }) + + t.Run("chain not in environment", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + env, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, []uint64{selector}), + ) + require.NoError(t, err) + + otherSel := chainselectors.TEST_90000002.Selector + _, err = BuildAcceptOwnershipTimelockProposal(*env, AcceptOwnershipProposalInput{ + ContractsByChain: map[uint64]string{ + otherSel: testAddr1, + }, + Description: "test", + Action: mcmstypes.TimelockActionBypass, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "not found in environment") + }) + + t.Run("fails when MCMS or timelock missing from datastore", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + env, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, []uint64{selector}), + ) + require.NoError(t, err) + + _, err = BuildAcceptOwnershipTimelockProposal(*env, AcceptOwnershipProposalInput{ + ContractsByChain: map[uint64]string{ + selector: testAddr1, + }, + Description: "test", + Action: mcmstypes.TimelockActionBypass, + }) + require.Error(t, err) + }) + + t.Run("builds proposal after deploy with custom description", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + cfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: {SetKeeperRegistryAddress: testAddr1}, + }, + } + out, err := DeployEthBalMonChangeSet.Apply(rt.Environment(), cfg) + require.NoError(t, err) + + contractsByChain := make(map[uint64]string, len(cfg.Chains)) + for sel := range cfg.Chains { + addr, err := GetContractAddress(out.DataStore, sel, cldf.ContractType(types.ETHBALMON_CONTRACT_TYPE)) + require.NoError(t, err) + contractsByChain[sel] = addr + } + + customDesc := "custom EthBalanceMonitor accept ownership proposal" + prop, err := BuildAcceptOwnershipTimelockProposal(rt.Environment(), AcceptOwnershipProposalInput{ + ContractsByChain: contractsByChain, + Description: customDesc, + Action: mcmstypes.TimelockActionBypass, + }) + require.NoError(t, err) + require.NotNil(t, prop) + require.Equal(t, customDesc, prop.Description) + require.Len(t, prop.Operations, len(cfg.Chains)) + }) + + t.Run("uses default description when empty", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + cfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: {SetKeeperRegistryAddress: testAddr1}, + }, + } + out, err := DeployEthBalMonChangeSet.Apply(rt.Environment(), cfg) + require.NoError(t, err) + + addr, err := GetContractAddress(out.DataStore, selector, cldf.ContractType(types.ETHBALMON_CONTRACT_TYPE)) + require.NoError(t, err) + + prop, err := BuildAcceptOwnershipTimelockProposal(rt.Environment(), AcceptOwnershipProposalInput{ + ContractsByChain: map[uint64]string{selector: addr}, + Description: "", + Action: mcmstypes.TimelockActionBypass, + }) + require.NoError(t, err) + require.Equal(t, "Accept ownership of EthBalanceMonitor across chains", prop.Description) + }) } func TestDeployEthBalMonChangeset(t *testing.T) { @@ -210,6 +321,38 @@ func TestDeployEthBalMonChangeset(t *testing.T) { require.Equal(t, uint64(60), uint64FromAny(t, mdMap["minWaitPeriodSeconds"])) }) + t.Run("explicit zero min wait uses default 60s", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + zero := uint64(0) + cfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: { + SetKeeperRegistryAddress: testAddr1, + SetMinWaitPeriodSeconds: &zero, + }, + }, + } + + out, err := DeployEthBalMonChangeSet.Apply(rt.Environment(), cfg) + require.NoError(t, err) + + mds, err := out.DataStore.ContractMetadata().Fetch() + require.NoError(t, err) + require.Len(t, mds, 1) + mdMap := contractMetadataMap(t, mds[0].Metadata) + require.Equal(t, uint64(60), uint64FromAny(t, mdMap["minWaitPeriodSeconds"])) + }) + t.Run("multiple chains", func(t *testing.T) { t.Parallel() @@ -253,6 +396,25 @@ func TestDeployEthBalMonChangeset(t *testing.T) { require.Contains(t, err.Error(), "chains must not be empty") }) + t.Run("verify preconditions rejects chain not in environment", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + otherSel := chainselectors.TEST_90000002.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + err = DeployEthBalMonChangeSet.VerifyPreconditions(rt.Environment(), types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + otherSel: {SetKeeperRegistryAddress: testAddr1}, + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "not found in environment") + }) + t.Run("apply without MCMS infrastructure fails", func(t *testing.T) { t.Parallel() @@ -276,6 +438,100 @@ func TestDeployEthBalMonChangeset(t *testing.T) { }) } +func TestDeployEthBalMon_RuntimeChangesetTask(t *testing.T) { + t.Parallel() + + t.Run("exec succeeds and merges EthBalMon into runtime datastore", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + cfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: {SetKeeperRegistryAddress: testAddr1}, + }, + } + + task := runtime.ChangesetTask(DeployEthBalMonChangeSet, cfg) + err = rt.Exec(task) + require.NoError(t, err) + + _, hasOutput := rt.State().Outputs[task.ID()] + require.True(t, hasOutput, "CLDF runtime should store ChangesetOutput under the task id") + + out := rt.State().Outputs[task.ID()] + require.NotNil(t, out.DataStore) + require.NotEmpty(t, out.MCMSTimelockProposals) + + records := rt.State().DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(selector), + datastore.AddressRefByType(datastore.ContractType(types.ETHBALMON_CONTRACT_TYPE)), + ) + require.Len(t, records, 1) + labelSet := records[0].Labels.List() + require.Contains(t, labelSet, types.ETHBALMON_CONTRACT_TYPE) + require.Contains(t, labelSet, "EthBalMonV1_0_0") + + assertEthBalMonDeployOutput(t, rt.Environment(), out, cfg) + }) + + t.Run("exec fails on invalid precondition", func(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + task := runtime.ChangesetTask(DeployEthBalMonChangeSet, types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{}, + }) + err = rt.Exec(task) + require.Error(t, err) + require.Contains(t, err.Error(), "chains must not be empty") + }) + + t.Run("multiple chains via runtime task", func(t *testing.T) { + t.Parallel() + + selector1 := chainselectors.TEST_90000001.Selector + selector2 := chainselectors.TEST_90000002.Selector + selectors := []uint64{selector1, selector2} + + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, selectors), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, selectors) + fundDeployerAccounts(t, rt.Environment(), selectors) + + cfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector1: {SetKeeperRegistryAddress: testAddr1}, + selector2: {SetKeeperRegistryAddress: testAddr2}, + }, + } + + task := runtime.ChangesetTask(DeployEthBalMonChangeSet, cfg) + require.NoError(t, rt.Exec(task)) + + out := rt.State().Outputs[task.ID()] + assertEthBalMonDeployOutput(t, rt.Environment(), out, cfg) + }) +} + // assertEthBalMonDeployOutput checks datastore, on-chain owner (post transferOwnership, pre-accept), // and the accept-ownership timelock proposal. func assertEthBalMonDeployOutput( From c3b46d04ab71daef5a91d412f085c277578e523a Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Fri, 8 May 2026 16:14:49 -0300 Subject: [PATCH 13/35] Add ethBalMon setWatchList validate configuration function --- .../vault/changeset/ethbalmon_setWatchList.go | 2 +- deployment/vault/changeset/validation.go | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/deployment/vault/changeset/ethbalmon_setWatchList.go b/deployment/vault/changeset/ethbalmon_setWatchList.go index cbc7d6a6c10..041fec6474a 100644 --- a/deployment/vault/changeset/ethbalmon_setWatchList.go +++ b/deployment/vault/changeset/ethbalmon_setWatchList.go @@ -25,7 +25,7 @@ type ethBalMonSetWatchList struct{} var EthBalMonSetWatchList cldf.ChangeSetV2[vaulttypes.EthBalMonSetWatchListInput] = ethBalMonSetWatchList{} func (sw ethBalMonSetWatchList) VerifyPreconditions(env cldf.Environment, config vaulttypes.EthBalMonSetWatchListInput) error { - return nil + return ValidateEthBalMonSetWatchListConfig(env.GetContext(), env, config) } diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index 36653188751..ef3f18f1ee0 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -312,3 +312,35 @@ func ValidateEthBalMonTransferOwnershipConfig(ctx context.Context, env cldf.Envi return nil } + +func ValidateEthBalMonSetWatchListConfig(ctx context.Context, env cldf.Environment, cfg types.EthBalMonSetWatchListInput) error { + if len(cfg.Chains) == 0 { + return fmt.Errorf("no chains provided") + } + + for chainSelector, chainConfig := range cfg.Chains { + if _, ok := env.BlockChains.EVMChains()[chainSelector]; !ok { + return fmt.Errorf("chain not found in environment: %d", chainSelector) + } + if len(chainConfig.Addresses) == 0 { + return fmt.Errorf("chain %d: addresses must not be empty", chainSelector) + } + if len(chainConfig.MinBalancesWei) == 0 { + return fmt.Errorf("chain %d: min_balance_wei must not be empty", chainSelector) + } + if len(chainConfig.TopUpAmountsWei) == 0 { + return fmt.Errorf("chain %d: topup_amounts_wei must not be empty", chainSelector) + } + for i, addr := range chainConfig.Addresses { + addrStr := addr.Hex() + if !common.IsHexAddress(addrStr) { + return fmt.Errorf("chain %d: address at index %d (%s) is invalid", chainSelector, i, addrStr) + } + if addrStr == "" || addrStr == "0x0000000000000000000000000000000000000000" { + return fmt.Errorf("chain %d: address at index %d is zero address", chainSelector, i) + } + } + } + + return nil +} From 87fb2e9aec2b7fda992151cea1eeba36e562498b Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Fri, 8 May 2026 16:51:21 -0300 Subject: [PATCH 14/35] Remove unused package --- deployment/go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/deployment/go.mod b/deployment/go.mod index 4b74782e443..47c742370a1 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -47,7 +47,6 @@ require ( github.com/smartcontractkit/chainlink-deployments-framework v0.100.0 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260507171202-46e6a397da2d github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 - github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260421142741-9c7fbaf7c828 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735 github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 From 5edb5abff00f3f548e134dd1c4d0215a81f0a7a9 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Fri, 8 May 2026 17:05:24 -0300 Subject: [PATCH 15/35] Fix lint feedback --- deployment/vault/changeset/ethbalmon_deploy.go | 6 +++--- .../vault/changeset/ethbalmon_setKeeperRegistryAddress.go | 3 ++- deployment/vault/changeset/ethbalmon_setWatchList.go | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy.go b/deployment/vault/changeset/ethbalmon_deploy.go index c73d2ece68b..0645ad861cb 100644 --- a/deployment/vault/changeset/ethbalmon_deploy.go +++ b/deployment/vault/changeset/ethbalmon_deploy.go @@ -2,6 +2,7 @@ package changeset import ( "encoding/json" + "errors" "fmt" "math/big" @@ -123,7 +124,6 @@ func (d deployEthBalMon) Apply(e cldf.Environment, config vaulttypes.DeployEthBa if err := memoryDataStore.ContractMetadata().Add(contractMetadata); err != nil { return cldf.ChangesetOutput{}, fmt.Errorf("failed to add contract metadata for chain %d: %w", chainOut.ChainSelector, err) } - } proposal, err := BuildAcceptOwnershipTimelockProposal( @@ -291,7 +291,7 @@ var DeployEthBalMonContractOperation = operations.NewOperation( chain.DeployerKey, chain.Client, keeperRegistryAddress, - new(big.Int).SetUint64(uint64(input.MinWaitPeriodSeconds)), // uint -> int -> bigint + new(big.Int).SetUint64(input.MinWaitPeriodSeconds), ) if err != nil { return DeployEthBalMonContractOutput{}, fmt.Errorf("failed to deploy EthBalanceMonitor: %w", err) @@ -414,7 +414,7 @@ func BuildAcceptOwnershipTimelockProposal( input AcceptOwnershipProposalInput, ) (*mcms.TimelockProposal, error) { if len(input.ContractsByChain) == 0 { - return nil, fmt.Errorf("no contracts provided to build accept ownership proposal") + return nil, errors.New("no contracts provided to build accept ownership proposal") } var batches []mcmstypes.BatchOperation diff --git a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go index 6838855f9ef..331ea03f416 100644 --- a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go +++ b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go @@ -2,6 +2,7 @@ package changeset import ( "encoding/json" + "errors" "fmt" "github.com/Masterminds/semver/v3" @@ -92,7 +93,7 @@ var SetKeeperRegistrySequence = operations.NewSequence( ) if len(input.Chains) == 0 { - return EthBalMonSetKeeperRegistryAddressSequenceOutput{}, fmt.Errorf("no chains provided") + return EthBalMonSetKeeperRegistryAddressSequenceOutput{}, errors.New("no chains provided") } var batches []mcmstypes.BatchOperation diff --git a/deployment/vault/changeset/ethbalmon_setWatchList.go b/deployment/vault/changeset/ethbalmon_setWatchList.go index 041fec6474a..a4e18cb618a 100644 --- a/deployment/vault/changeset/ethbalmon_setWatchList.go +++ b/deployment/vault/changeset/ethbalmon_setWatchList.go @@ -26,7 +26,6 @@ var EthBalMonSetWatchList cldf.ChangeSetV2[vaulttypes.EthBalMonSetWatchListInput func (sw ethBalMonSetWatchList) VerifyPreconditions(env cldf.Environment, config vaulttypes.EthBalMonSetWatchListInput) error { return ValidateEthBalMonSetWatchListConfig(env.GetContext(), env, config) - } func (sw ethBalMonSetWatchList) Apply(e cldf.Environment, config vaulttypes.EthBalMonSetWatchListInput) (cldf.ChangesetOutput, error) { From aae63ef1c6954c62b2885d37cb196a25391c9a63 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Fri, 8 May 2026 17:24:59 -0300 Subject: [PATCH 16/35] Fix lint feedback --- .../vault/changeset/ethbalmon_deploy.go | 18 +++++---- .../vault/changeset/ethbalmon_deploy_test.go | 38 ++++++++++++------- .../ethbalmon_setKeeperRegistryAddress.go | 13 ++++--- .../vault/changeset/ethbalmon_setWatchList.go | 13 ++++--- .../changeset/ethbalmon_transferOwnership.go | 13 ++++--- .../vault/changeset/ethbalmon_withdraw.go | 13 ++++--- deployment/vault/changeset/types/types.go | 3 +- deployment/vault/changeset/validation.go | 13 +++---- 8 files changed, 70 insertions(+), 54 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy.go b/deployment/vault/changeset/ethbalmon_deploy.go index 0645ad861cb..fc7bdd8c399 100644 --- a/deployment/vault/changeset/ethbalmon_deploy.go +++ b/deployment/vault/changeset/ethbalmon_deploy.go @@ -9,19 +9,21 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" - cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" "github.com/smartcontractkit/mcms" mcmssdk "github.com/smartcontractkit/mcms/sdk" mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" + + mcmstypes "github.com/smartcontractkit/mcms/types" + ds "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/operations" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" - mcmstypes "github.com/smartcontractkit/mcms/types" ) const defaultEthBalMonMinWaitPeriodSeconds uint64 = 60 @@ -95,11 +97,11 @@ func (d deployEthBalMon) Apply(e cldf.Environment, config vaulttypes.DeployEthBa addressRef := ds.AddressRef{ ChainSelector: chainOut.ChainSelector, Address: chainOut.ContractAddress, - Type: ds.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), + Type: ds.ContractType(vaulttypes.EthBalMonContractType), Version: semver.MustParse("1.0.0"), - Qualifier: fmt.Sprintf("%s:%s", vaulttypes.ETHBALMON_CONTRACT_TYPE, chainOut.ContractAddress), + Qualifier: fmt.Sprintf("%s:%s", vaulttypes.EthBalMonContractType, chainOut.ContractAddress), Labels: ds.NewLabelSet( - vaulttypes.ETHBALMON_CONTRACT_TYPE, + vaulttypes.EthBalMonContractType, "EthBalMonV1_0_0", ), } @@ -473,7 +475,7 @@ func BuildAcceptOwnershipTimelockProposal( Transactions: []mcmstypes.Transaction{ { OperationMetadata: mcmstypes.OperationMetadata{ - ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, + ContractType: vaulttypes.EthBalMonContractType, Tags: []string{"acceptOwnership"}, }, To: contractAddr, diff --git a/deployment/vault/changeset/ethbalmon_deploy_test.go b/deployment/vault/changeset/ethbalmon_deploy_test.go index d635a86351f..5ab42afdf11 100644 --- a/deployment/vault/changeset/ethbalmon_deploy_test.go +++ b/deployment/vault/changeset/ethbalmon_deploy_test.go @@ -11,12 +11,13 @@ import ( "github.com/stretchr/testify/require" chainselectors "github.com/smartcontractkit/chain-selectors" + mcmstypes "github.com/smartcontractkit/mcms/types" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" - mcmstypes "github.com/smartcontractkit/mcms/types" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" @@ -162,7 +163,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { otherSel: testAddr1, }, Description: "test", - Action: mcmstypes.TimelockActionBypass, + Action: mcmstypes.TimelockActionBypass, }) require.Error(t, err) require.Contains(t, err.Error(), "not found in environment") @@ -182,7 +183,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { selector: testAddr1, }, Description: "test", - Action: mcmstypes.TimelockActionBypass, + Action: mcmstypes.TimelockActionBypass, }) require.Error(t, err) }) @@ -209,7 +210,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { contractsByChain := make(map[uint64]string, len(cfg.Chains)) for sel := range cfg.Chains { - addr, err := GetContractAddress(out.DataStore, sel, cldf.ContractType(types.ETHBALMON_CONTRACT_TYPE)) + addr, err := GetContractAddress(out.DataStore, sel, cldf.ContractType(types.EthBalMonContractType)) require.NoError(t, err) contractsByChain[sel] = addr } @@ -246,7 +247,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { out, err := DeployEthBalMonChangeSet.Apply(rt.Environment(), cfg) require.NoError(t, err) - addr, err := GetContractAddress(out.DataStore, selector, cldf.ContractType(types.ETHBALMON_CONTRACT_TYPE)) + addr, err := GetContractAddress(out.DataStore, selector, cldf.ContractType(types.EthBalMonContractType)) require.NoError(t, err) prop, err := BuildAcceptOwnershipTimelockProposal(rt.Environment(), AcceptOwnershipProposalInput{ @@ -472,11 +473,11 @@ func TestDeployEthBalMon_RuntimeChangesetTask(t *testing.T) { records := rt.State().DataStore.Addresses().Filter( datastore.AddressRefByChainSelector(selector), - datastore.AddressRefByType(datastore.ContractType(types.ETHBALMON_CONTRACT_TYPE)), + datastore.AddressRefByType(datastore.ContractType(types.EthBalMonContractType)), ) require.Len(t, records, 1) labelSet := records[0].Labels.List() - require.Contains(t, labelSet, types.ETHBALMON_CONTRACT_TYPE) + require.Contains(t, labelSet, types.EthBalMonContractType) require.Contains(t, labelSet, "EthBalMonV1_0_0") assertEthBalMonDeployOutput(t, rt.Environment(), out, cfg) @@ -580,7 +581,7 @@ func assertEthBalMonDeployOutput( require.Equal(t, mcmsAddr, md["mcmsAddress"]) require.NotEmpty(t, md["transferOwnershipTxHash"]) - ebmAddr, err := GetContractAddress(out.DataStore, sel, cldf.ContractType(types.ETHBALMON_CONTRACT_TYPE)) + ebmAddr, err := GetContractAddress(out.DataStore, sel, cldf.ContractType(types.EthBalMonContractType)) require.NoError(t, err) chain := env.BlockChains.EVMChains()[sel] @@ -603,10 +604,10 @@ func assertEthBalMonDeployOutput( seen[sel] = true require.Len(t, op.Transactions, 1) tx := op.Transactions[0] - require.Equal(t, types.ETHBALMON_CONTRACT_TYPE, tx.OperationMetadata.ContractType) - require.Contains(t, tx.OperationMetadata.Tags, "acceptOwnership") + require.Equal(t, types.EthBalMonContractType, tx.ContractType) + require.Contains(t, tx.Tags, "acceptOwnership") - wantContract, err := GetContractAddress(out.DataStore, sel, cldf.ContractType(types.ETHBALMON_CONTRACT_TYPE)) + wantContract, err := GetContractAddress(out.DataStore, sel, cldf.ContractType(types.EthBalMonContractType)) require.NoError(t, err) require.Equal(t, common.HexToAddress(wantContract), common.HexToAddress(tx.To)) } @@ -641,15 +642,24 @@ func uint64FromAny(t *testing.T, v any) uint64 { case uint32: return uint64(x) case int: - return uint64(x) + require.GreaterOrEqual(t, x, 0) + u, err := strconv.ParseUint(strconv.Itoa(x), 10, 64) + require.NoError(t, err) + return u case int64: - return uint64(x) + require.GreaterOrEqual(t, x, int64(0)) + u, err := strconv.ParseUint(strconv.FormatInt(x, 10), 10, 64) + require.NoError(t, err) + return u case float64: return uint64(x) case json.Number: i, err := x.Int64() require.NoError(t, err) - return uint64(i) + require.GreaterOrEqual(t, i, int64(0)) + u, err := strconv.ParseUint(strconv.FormatInt(i, 10), 10, 64) + require.NoError(t, err) + return u case string: u, err := strconv.ParseUint(x, 10, 64) require.NoError(t, err) diff --git a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go index 331ea03f416..26043f5877d 100644 --- a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go +++ b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go @@ -7,6 +7,11 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/mcms" + mcmssdk "github.com/smartcontractkit/mcms/sdk" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + mcmstypes "github.com/smartcontractkit/mcms/types" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -14,10 +19,6 @@ import ( "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" - "github.com/smartcontractkit/mcms" - mcmssdk "github.com/smartcontractkit/mcms/sdk" - mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" - mcmstypes "github.com/smartcontractkit/mcms/types" ) type setKeeperRegistryAddress struct{} @@ -179,7 +180,7 @@ var SetKeeperRegistryOperation = operations.NewOperation( ethBalMonAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - cldf.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), + cldf.ContractType(vaulttypes.EthBalMonContractType), ) if err != nil { return SetKeeperRegistryOperationOutput{}, @@ -229,7 +230,7 @@ var SetKeeperRegistryOperation = operations.NewOperation( Transactions: []mcmstypes.Transaction{ { OperationMetadata: mcmstypes.OperationMetadata{ - ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, + ContractType: vaulttypes.EthBalMonContractType, Tags: []string{ "setKeeperRegistryAddress", }, diff --git a/deployment/vault/changeset/ethbalmon_setWatchList.go b/deployment/vault/changeset/ethbalmon_setWatchList.go index a4e18cb618a..72f492a7a21 100644 --- a/deployment/vault/changeset/ethbalmon_setWatchList.go +++ b/deployment/vault/changeset/ethbalmon_setWatchList.go @@ -7,6 +7,11 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/mcms" + mcmssdk "github.com/smartcontractkit/mcms/sdk" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + mcmstypes "github.com/smartcontractkit/mcms/types" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -14,10 +19,6 @@ import ( "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" - "github.com/smartcontractkit/mcms" - mcmssdk "github.com/smartcontractkit/mcms/sdk" - mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" - mcmstypes "github.com/smartcontractkit/mcms/types" ) type ethBalMonSetWatchList struct{} @@ -148,7 +149,7 @@ var EthBalMonSetWatchListOperation = operations.NewOperation( ethBalMonAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - cldf.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), + cldf.ContractType(vaulttypes.EthBalMonContractType), ) if err != nil { return EthBalMonSetWatchListOpOutput{}, @@ -202,7 +203,7 @@ var EthBalMonSetWatchListOperation = operations.NewOperation( Transactions: []mcmstypes.Transaction{ { OperationMetadata: mcmstypes.OperationMetadata{ - ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, + ContractType: vaulttypes.EthBalMonContractType, Tags: []string{ "setWatchList", }, diff --git a/deployment/vault/changeset/ethbalmon_transferOwnership.go b/deployment/vault/changeset/ethbalmon_transferOwnership.go index c3fdb965bee..2ca56d51a26 100644 --- a/deployment/vault/changeset/ethbalmon_transferOwnership.go +++ b/deployment/vault/changeset/ethbalmon_transferOwnership.go @@ -6,6 +6,11 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/mcms" + mcmssdk "github.com/smartcontractkit/mcms/sdk" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + mcmstypes "github.com/smartcontractkit/mcms/types" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -13,10 +18,6 @@ import ( "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" - "github.com/smartcontractkit/mcms" - mcmssdk "github.com/smartcontractkit/mcms/sdk" - mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" - mcmstypes "github.com/smartcontractkit/mcms/types" ) type ethBalMonTransferOwnership struct{} @@ -140,7 +141,7 @@ var EthBalMonTransferOwnershipOperation = operations.NewOperation( ethBalMonAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - cldf.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), + cldf.ContractType(vaulttypes.EthBalMonContractType), ) if err != nil { return EthBalMonTransferOwnershipOpOutput{}, @@ -181,7 +182,7 @@ var EthBalMonTransferOwnershipOperation = operations.NewOperation( Transactions: []mcmstypes.Transaction{ { OperationMetadata: mcmstypes.OperationMetadata{ - ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, + ContractType: vaulttypes.EthBalMonContractType, Tags: []string{ "transferOwnership", }, diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go index 13c9000436c..917970d7355 100644 --- a/deployment/vault/changeset/ethbalmon_withdraw.go +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -7,6 +7,11 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/mcms" + mcmssdk "github.com/smartcontractkit/mcms/sdk" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + mcmstypes "github.com/smartcontractkit/mcms/types" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -14,10 +19,6 @@ import ( "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" - "github.com/smartcontractkit/mcms" - mcmssdk "github.com/smartcontractkit/mcms/sdk" - mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" - mcmstypes "github.com/smartcontractkit/mcms/types" ) type ethBalMonWithdraw struct{} @@ -143,7 +144,7 @@ var EthBalMonWithdrawOperation = operations.NewOperation( ethBalMonAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - cldf.ContractType(vaulttypes.ETHBALMON_CONTRACT_TYPE), + cldf.ContractType(vaulttypes.EthBalMonContractType), ) if err != nil { return EthBalMonWithdrawOpOutput{}, @@ -185,7 +186,7 @@ var EthBalMonWithdrawOperation = operations.NewOperation( Transactions: []mcmstypes.Transaction{ { OperationMetadata: mcmstypes.OperationMetadata{ - ContractType: vaulttypes.ETHBALMON_CONTRACT_TYPE, + ContractType: vaulttypes.EthBalMonContractType, Tags: []string{ "withdraw", }, diff --git a/deployment/vault/changeset/types/types.go b/deployment/vault/changeset/types/types.go index 33a25379ba3..524c797a4c6 100644 --- a/deployment/vault/changeset/types/types.go +++ b/deployment/vault/changeset/types/types.go @@ -4,6 +4,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" ) @@ -91,7 +92,7 @@ type DeployEthBalMonInput struct { Chains map[uint64]DeployEthBalMonChainConfig `json:"chains"` } -const ETHBALMON_CONTRACT_TYPE string = "EthBalMon" +const EthBalMonContractType = "EthBalMon" // setKeeperRegistryAddress config type SetKeeperRegistryChainConfig struct { diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index 38acdc9ead3..7a808e5bb3f 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -11,9 +11,8 @@ import ( cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - evmstate "github.com/smartcontractkit/cld-changesets/pkg/family/evm" - "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" @@ -139,7 +138,7 @@ func validateMCMSConfig(e cldf.Environment, mcmsConfig *proposalutils.TimelockCo } const emptyQualifier = "" for chainSelector := range transfersByChain { - addresses, err := evmstate.LoadAddressesFromDataStore(e.DataStore, chainSelector, emptyQualifier) + addresses, err := state.GetAddressTypeVersionByQualifier(e.DataStore.Addresses(), chainSelector, emptyQualifier) if err != nil { return fmt.Errorf("failed to get addresses from datastore for chain %d: %w", chainSelector, err) } @@ -255,7 +254,7 @@ func ValidateDeployEthBalMonConfig(ctx context.Context, env cldf.Environment, cf func ValidateSetKeeperRegistryAddressConfig(ctx context.Context, env cldf.Environment, cfg types.EthBalMonSetKeeperRegistryAddressInput) error { if len(cfg.Chains) == 0 { - return fmt.Errorf("no chains provided") + return errors.New("no chains provided") } for chainSelector, chainConfig := range cfg.Chains { @@ -273,7 +272,7 @@ func ValidateSetKeeperRegistryAddressConfig(ctx context.Context, env cldf.Enviro func ValidateEthBalMonWithdrawConfig(ctx context.Context, env cldf.Environment, cfg types.EthBalMonWithdrawInput) error { if len(cfg.Chains) == 0 { - return fmt.Errorf("no chains provided") + return errors.New("no chains provided") } for chainSelector, chainConfig := range cfg.Chains { @@ -296,7 +295,7 @@ func ValidateEthBalMonWithdrawConfig(ctx context.Context, env cldf.Environment, func ValidateEthBalMonTransferOwnershipConfig(ctx context.Context, env cldf.Environment, cfg types.EthBalMonTransferOwnershipInput) error { if len(cfg.Chains) == 0 { - return fmt.Errorf("no chains provided") + return errors.New("no chains provided") } for chainSelector, chainConfig := range cfg.Chains { @@ -316,7 +315,7 @@ func ValidateEthBalMonTransferOwnershipConfig(ctx context.Context, env cldf.Envi func ValidateEthBalMonSetWatchListConfig(ctx context.Context, env cldf.Environment, cfg types.EthBalMonSetWatchListInput) error { if len(cfg.Chains) == 0 { - return fmt.Errorf("no chains provided") + return errors.New("no chains provided") } for chainSelector, chainConfig := range cfg.Chains { From 01006e9a005427535d976fdef350d392bc71e750 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Mon, 11 May 2026 12:56:31 -0300 Subject: [PATCH 17/35] Fix lint --- deployment/vault/changeset/ethbalmon_deploy.go | 2 +- deployment/vault/changeset/validation.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy.go b/deployment/vault/changeset/ethbalmon_deploy.go index fc7bdd8c399..112c92b9fc0 100644 --- a/deployment/vault/changeset/ethbalmon_deploy.go +++ b/deployment/vault/changeset/ethbalmon_deploy.go @@ -197,7 +197,7 @@ var DeployEthBalMonSequence = operations.NewSequence( rawMinWait = *chainConfig.SetMinWaitPeriodSeconds } minWait := effectiveMinWaitPeriodSeconds(rawMinWait) - timelockAddr, err := mustGetContractAddress(deps.DataStore, chainSelector, commontypes.RBACTimelock) + timelockAddr, err := mustGetContractAddress(deps.DataStore, chainSelector, cldf.ContractType(commontypes.RBACTimelock)) if err != nil { return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain %d: failed to get timelock address: %w", chainSelector, err) } diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index 570366be70b..7a808e5bb3f 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -11,8 +11,6 @@ import ( cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - evmstate "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/evm" - "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/deployment/common/changeset/state" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" From 88960cbcaed5441799ad0be5b8687881ae9e936f Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Mon, 11 May 2026 13:03:06 -0300 Subject: [PATCH 18/35] Remove unnecesary conversion --- deployment/vault/changeset/ethbalmon_deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy.go b/deployment/vault/changeset/ethbalmon_deploy.go index 112c92b9fc0..fc7bdd8c399 100644 --- a/deployment/vault/changeset/ethbalmon_deploy.go +++ b/deployment/vault/changeset/ethbalmon_deploy.go @@ -197,7 +197,7 @@ var DeployEthBalMonSequence = operations.NewSequence( rawMinWait = *chainConfig.SetMinWaitPeriodSeconds } minWait := effectiveMinWaitPeriodSeconds(rawMinWait) - timelockAddr, err := mustGetContractAddress(deps.DataStore, chainSelector, cldf.ContractType(commontypes.RBACTimelock)) + timelockAddr, err := mustGetContractAddress(deps.DataStore, chainSelector, commontypes.RBACTimelock) if err != nil { return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain %d: failed to get timelock address: %w", chainSelector, err) } From 04d17aad462df76b858e2a1e3a175039ebf1622a Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Mon, 11 May 2026 14:14:42 -0300 Subject: [PATCH 19/35] Remove wrong comment --- deployment/vault/changeset/ethbalmon_deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy.go b/deployment/vault/changeset/ethbalmon_deploy.go index fc7bdd8c399..f24ed58b12a 100644 --- a/deployment/vault/changeset/ethbalmon_deploy.go +++ b/deployment/vault/changeset/ethbalmon_deploy.go @@ -401,7 +401,7 @@ var TransferOwnershipOperation = operations.NewOperation( // ====================================================== // ====================================================== -// Operation 3: Build accept ownership batch OPERATION +// Operation 3: Build accept ownership batch // ====================================================== // ====================================================== From 87112705edbc3cba611c4fadd8771eb0a771d7b6 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Mon, 11 May 2026 15:56:32 -0300 Subject: [PATCH 20/35] Add useful comments for types --- deployment/vault/changeset/types/types.go | 44 +++++++++++++++-------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/deployment/vault/changeset/types/types.go b/deployment/vault/changeset/types/types.go index 524c797a4c6..7f4271ff039 100644 --- a/deployment/vault/changeset/types/types.go +++ b/deployment/vault/changeset/types/types.go @@ -74,60 +74,74 @@ type BatchNativeTransferState struct { ValidationErrors []TransferValidationError `json:"validation_errors"` } -// DeployEthBalMonChainConfig is the EthBalMon configuration for a single chain. -// -// SetKeeperRegistryAddress is the Chainlink Automation registry forwarder address (from the -// upkeep "forwarder address" on chains with Chainlink automation) or the -// KMS executor address on chains that use the Plaid/KMS automation path instead. -// -// SetMinWaitPeriodSeconds is optional; when omitted or non-positive it defaults to 60. +// DeployEthBalMonChainConfig is deployment-time configuration for EthBalMon on one chain. type DeployEthBalMonChainConfig struct { - SetKeeperRegistryAddress string `json:"setKeeperRegistryAddress"` - SetMinWaitPeriodSeconds *uint64 `json:"setMinWaitPeriodSeconds,omitempty"` + // SetKeeperRegistryAddress is the Chainlink Automation registry forwarder (the upkeep + // "forwarder address") on standard automation chains, or the KMS executor address when + // using the Plaid/KMS automation path. + SetKeeperRegistryAddress string `json:"setKeeperRegistryAddress"` + // SetMinWaitPeriodSeconds is the minimum seconds between balance checks for this deployment. + // Optional: nil or 0 means the deploy changeset uses a default (currently 60 seconds). + SetMinWaitPeriodSeconds *uint64 `json:"setMinWaitPeriodSeconds,omitempty"` } -// DeployEthBalMonInput configures EthBalMon deployment across chains. -// Map keys are chain selectors; each entry is one chain’s keeper/KMS and watch list. +// DeployEthBalMonInput is the input to the EthBalMon deploy changeset. +// Keys are chain selectors; each value configures keeper/registry wiring and min wait for that chain. type DeployEthBalMonInput struct { Chains map[uint64]DeployEthBalMonChainConfig `json:"chains"` } +// EthBalMonContractType is the datastore / MCMS contract type label for EthBalMon deployments. const EthBalMonContractType = "EthBalMon" -// setKeeperRegistryAddress config +// SetKeeperRegistryChainConfig updates the automation executor/registry EthBalMon forwards work to. type SetKeeperRegistryChainConfig struct { + // NewKeeperRegistryAddress is the new Chainlink Automation forwarder or KMS executor address (hex). NewKeeperRegistryAddress string `json:"new_keeper_registry_address"` } +// EthBalMonSetKeeperRegistryAddressInput is the input to the setKeeperRegistryAddress changeset. +// Keys are chain selectors with the registry address to set on each chain's EthBalMon. type EthBalMonSetKeeperRegistryAddressInput struct { Chains map[uint64]SetKeeperRegistryChainConfig `json:"chains"` } -// setWatchList config +// EthBalMonSetWatchListChainConfig replaces the monitored addresses and thresholds on one chain. +// Addresses, MinBalancesWei, and TopUpAmountsWei are parallel slices: index i applies to Addresses[i]. type EthBalMonSetWatchListChainConfig struct { Addresses []common.Address `json:"addresses"` MinBalancesWei []big.Int `json:"min_balance_wei"` TopUpAmountsWei []big.Int `json:"topup_amounts_wei"` } + +// EthBalMonSetWatchListInput is the input to the setWatchList changeset. +// Keys are chain selectors; each value is the full watch list to install on that chain's EthBalMon. type EthBalMonSetWatchListInput struct { Chains map[uint64]EthBalMonSetWatchListChainConfig `json:"chains"` } -// withdraw config +// EthBalMonWithdrawChainConfig configures a native-token withdraw from EthBalMon on one chain. type EthBalMonWithdrawChainConfig struct { + // Amount is the withdrawal amount in wei. Must be non-zero (validated by the changeset). Amount uint64 `json:"amount"` + // Payeer is the recipient address (hex). JSON key is "payeer" for backward compatibility. Payeer string `json:"payeer"` } +// EthBalMonWithdrawInput is the input to the EthBalMon withdraw changeset. +// Keys are chain selectors; each value specifies amount and recipient for that chain. type EthBalMonWithdrawInput struct { Chains map[uint64]EthBalMonWithdrawChainConfig `json:"chains"` } -// transferOwnership config +// EthBalMonTransferOwnershipChainConfig sets the new owner of EthBalMon on one chain. type EthBalMonTransferOwnershipChainConfig struct { + // NewOwner is the address (hex) that will own the EthBalMon contract after the operation. NewOwner string `json:"new_owner"` } +// EthBalMonTransferOwnershipInput is the input to the EthBalMon transferOwnership changeset. +// Keys are chain selectors; each value is the new owner for that chain's EthBalMon instance. type EthBalMonTransferOwnershipInput struct { Chains map[uint64]EthBalMonTransferOwnershipChainConfig `json:"chains"` } From 666ecaad4bab7091297c50ea12e09fdfd01048e1 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Mon, 11 May 2026 16:49:22 -0300 Subject: [PATCH 21/35] Change log info --- deployment/vault/changeset/ethbalmon_withdraw.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go index 917970d7355..1fcd28e24b8 100644 --- a/deployment/vault/changeset/ethbalmon_withdraw.go +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -74,7 +74,7 @@ var EthBalMonWithdrawSequence = operations.NewSequence( "Sequence to create operation for EthBalMon withdraw", func(b operations.Bundle, deps VaultDeps, input EthBalMonWithdrawSeqInput) (EthBalMonWithdrawSeqOutput, error) { b.Logger.Infow("Starting EthBalMon withdraw sequence", - "chains", len(input.Chains), + "numChains", len(input.Chains), ) var batches []mcmstypes.BatchOperation timelockAddresses := make(map[uint64]string) From 8dbcab1d3c50afc8c0eb8b4326edc68c18a707a3 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Tue, 12 May 2026 10:58:01 -0300 Subject: [PATCH 22/35] Get output after check error --- deployment/vault/changeset/ethbalmon_setWatchList.go | 3 ++- deployment/vault/changeset/ethbalmon_transferOwnership.go | 3 ++- deployment/vault/changeset/ethbalmon_withdraw.go | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_setWatchList.go b/deployment/vault/changeset/ethbalmon_setWatchList.go index 72f492a7a21..8dbf28cdbaa 100644 --- a/deployment/vault/changeset/ethbalmon_setWatchList.go +++ b/deployment/vault/changeset/ethbalmon_setWatchList.go @@ -90,10 +90,11 @@ var EthBalMonSetWatchListSequence = operations.NewSequence( MinBalancesWei: chainConfig.MinBalancesWei, TopUpAmountsWei: chainConfig.TopUpAmountsWei, }) - opOutput := opReport.Output if err != nil { return EthBalMonSetWatchListSeqOutput{}, fmt.Errorf("chain %d: failed to generate setWatchList batch: %w", chainSelector, err) } + opOutput := opReport.Output + batches = append(batches, opOutput.BatchOperation) timelockAddresses[chainSelector] = opOutput.TimelockAddress mcmAddressByChain[chainSelector] = opOutput.MCMSAddress diff --git a/deployment/vault/changeset/ethbalmon_transferOwnership.go b/deployment/vault/changeset/ethbalmon_transferOwnership.go index 2ca56d51a26..f7bd5c2a378 100644 --- a/deployment/vault/changeset/ethbalmon_transferOwnership.go +++ b/deployment/vault/changeset/ethbalmon_transferOwnership.go @@ -84,10 +84,11 @@ var EthBalMonTransferOwnershipSequence = operations.NewSequence( ChainSelector: chainSelector, NewOwner: chainConfig.NewOwner, }) - opOutput := opReport.Output if err != nil { return EthBalMonTransferOwnershipSeqOutput{}, fmt.Errorf("chain %d: failed to generate ownership batch: %w", chainSelector, err) } + opOutput := opReport.Output + batches = append(batches, opOutput.BatchOperation) timelockAddresses[chainSelector] = opOutput.TimelockAddress mcmAddressByChain[chainSelector] = opOutput.MCMSAddress diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go index 1fcd28e24b8..a0e61c6791b 100644 --- a/deployment/vault/changeset/ethbalmon_withdraw.go +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -86,10 +86,11 @@ var EthBalMonWithdrawSequence = operations.NewSequence( Amount: chainConfig.Amount, Payeer: chainConfig.Payeer, }) - opOutput := opReport.Output if err != nil { return EthBalMonWithdrawSeqOutput{}, fmt.Errorf("chain %d: failed to generate withdraw batch: %w", chainSelector, err) } + opOutput := opReport.Output + batches = append(batches, opOutput.BatchOperation) timelockAddresses[chainSelector] = opOutput.TimelockAddress mcmAddressByChain[chainSelector] = opOutput.MCMSAddress From 466268a4e73f55ec7e49b98395acabcb4f390d47 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Tue, 12 May 2026 11:01:53 -0300 Subject: [PATCH 23/35] Fix validations --- deployment/vault/changeset/validation.go | 27 ++++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index 7a808e5bb3f..dd8bcddc136 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -282,12 +282,12 @@ func ValidateEthBalMonWithdrawConfig(ctx context.Context, env cldf.Environment, if chainConfig.Amount == 0 { return fmt.Errorf("chain %d: amount to withdraw cannot be 0 (zero)", chainSelector) } - if !common.IsHexAddress(chainConfig.Payeer) { - return fmt.Errorf("chain %d: invalid payeer address: %s", chainSelector, chainConfig.Payeer) - } - if chainConfig.Payeer == "" || chainConfig.Payeer == "0x0000000000000000000000000000000000000000" { - return fmt.Errorf("chain %d: payeer address cannot be zero address", chainSelector) + + err := validateEthAddress("payeer", chainConfig.Payeer) + if err != nil { + return fmt.Errorf("chain %d: %w", chainSelector, err) } + } return nil @@ -302,11 +302,9 @@ func ValidateEthBalMonTransferOwnershipConfig(ctx context.Context, env cldf.Envi if _, ok := env.BlockChains.EVMChains()[chainSelector]; !ok { return fmt.Errorf("chain not found in environment: %d", chainSelector) } - if !common.IsHexAddress(chainConfig.NewOwner) { - return fmt.Errorf("chain %d: invalid payeer address: %s", chainSelector, chainConfig.NewOwner) - } - if chainConfig.NewOwner == "" || chainConfig.NewOwner == "0x0000000000000000000000000000000000000000" { - return fmt.Errorf("chain %d: payeer address cannot be zero address", chainSelector) + err := validateEthAddress("newOwner", chainConfig.NewOwner) + if err != nil { + return fmt.Errorf("chain %d: %w", chainSelector, err) } } @@ -332,12 +330,9 @@ func ValidateEthBalMonSetWatchListConfig(ctx context.Context, env cldf.Environme return fmt.Errorf("chain %d: topup_amounts_wei must not be empty", chainSelector) } for i, addr := range chainConfig.Addresses { - addrStr := addr.Hex() - if !common.IsHexAddress(addrStr) { - return fmt.Errorf("chain %d: address at index %d (%s) is invalid", chainSelector, i, addrStr) - } - if addrStr == "" || addrStr == "0x0000000000000000000000000000000000000000" { - return fmt.Errorf("chain %d: address at index %d is zero address", chainSelector, i) + err := validateEthAddress(fmt.Sprintf("address %d", i), addr.Hex()) + if err != nil { + return fmt.Errorf("chain %d: %w", chainSelector, err) } } } From 0b44240b7fa135372726e96a306161d7acb69fa4 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Tue, 12 May 2026 11:06:54 -0300 Subject: [PATCH 24/35] Change withdraw config amount type --- .../vault/changeset/ethbalmon_setWatchList.go | 4 +-- .../changeset/ethbalmon_transferOwnership.go | 2 +- .../vault/changeset/ethbalmon_withdraw.go | 9 +++-- deployment/vault/changeset/types/types.go | 4 +-- deployment/vault/changeset/validation.go | 36 +++++++++++-------- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_setWatchList.go b/deployment/vault/changeset/ethbalmon_setWatchList.go index 8dbf28cdbaa..58aec3042e3 100644 --- a/deployment/vault/changeset/ethbalmon_setWatchList.go +++ b/deployment/vault/changeset/ethbalmon_setWatchList.go @@ -71,7 +71,7 @@ type EthBalMonSetWatchListSeqOutput struct { } var EthBalMonSetWatchListSequence = operations.NewSequence( - "ethbalmon-setWathcList-sequence", + "ethbalmon-setWatchList-sequence", semver.MustParse("1.0.0"), "Sequence to create operations for EthBalMon setWatchList", func(b operations.Bundle, deps VaultDeps, input EthBalMonSetWatchListSeqInput) (EthBalMonSetWatchListSeqOutput, error) { @@ -133,7 +133,7 @@ type EthBalMonSetWatchListOpOutput struct { } var EthBalMonSetWatchListOperation = operations.NewOperation( - "ethbalmon-setWathcList-operation", + "ethbalmon-setWatchList-operation", semver.MustParse("1.0.0"), "Operation to create transaction batch for EthBalMon setWatchList", func(b operations.Bundle, deps VaultDeps, input EthBalMonSetWatchListOpInput) (EthBalMonSetWatchListOpOutput, error) { diff --git a/deployment/vault/changeset/ethbalmon_transferOwnership.go b/deployment/vault/changeset/ethbalmon_transferOwnership.go index f7bd5c2a378..d7c22fd012b 100644 --- a/deployment/vault/changeset/ethbalmon_transferOwnership.go +++ b/deployment/vault/changeset/ethbalmon_transferOwnership.go @@ -68,7 +68,7 @@ type EthBalMonTransferOwnershipSeqOutput struct { } var EthBalMonTransferOwnershipSequence = operations.NewSequence( - "ethbalmon-transferownership-operation", + "ethbalmon-transferownership-sequence", semver.MustParse("1.0.0"), "Sequence to create transferOwnership EthBalMon batch transaction", func(b operations.Bundle, deps VaultDeps, input EthBalMonTransferOwnershipSeqInput) (EthBalMonTransferOwnershipSeqOutput, error) { diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go index a0e61c6791b..3d500831416 100644 --- a/deployment/vault/changeset/ethbalmon_withdraw.go +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -114,9 +114,9 @@ var EthBalMonWithdrawSequence = operations.NewSequence( ) type EthBalMonWithdrawOpInput struct { - ChainSelector uint64 `json:"chain_selector"` - Amount uint64 `json:"amount"` - Payeer string `json:"payeer"` + ChainSelector uint64 `json:"chain_selector"` + Amount *big.Int `json:"amount"` + Payeer string `json:"payeer"` } type EthBalMonWithdrawOpOutput struct { @@ -177,8 +177,7 @@ var EthBalMonWithdrawOperation = operations.NewOperation( fmt.Errorf("failed to instantiate EthBalanceMonitor at %s: %w", ethBalMonAddr, err) } - amountBigInt := big.NewInt(int64(input.Amount)) - withdrawTx, err := ethBalMon.Withdraw(cldf.SimTransactOpts(), amountBigInt, common.HexToAddress(input.Payeer)) + withdrawTx, err := ethBalMon.Withdraw(cldf.SimTransactOpts(), input.Amount, common.HexToAddress(input.Payeer)) if err != nil { return EthBalMonWithdrawOpOutput{}, fmt.Errorf("failed to generate withdraw calldata on chain %d: %w ", input.ChainSelector, err) } diff --git a/deployment/vault/changeset/types/types.go b/deployment/vault/changeset/types/types.go index 7f4271ff039..abb7b69df79 100644 --- a/deployment/vault/changeset/types/types.go +++ b/deployment/vault/changeset/types/types.go @@ -122,8 +122,8 @@ type EthBalMonSetWatchListInput struct { // EthBalMonWithdrawChainConfig configures a native-token withdraw from EthBalMon on one chain. type EthBalMonWithdrawChainConfig struct { - // Amount is the withdrawal amount in wei. Must be non-zero (validated by the changeset). - Amount uint64 `json:"amount"` + // Amount is the withdrawal amount in wei. Must be positive (validated by the changeset). + Amount *big.Int `json:"amount"` // Payeer is the recipient address (hex). JSON key is "payeer" for backward compatibility. Payeer string `json:"payeer"` } diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index dd8bcddc136..b3b6096cde5 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -279,15 +279,16 @@ func ValidateEthBalMonWithdrawConfig(ctx context.Context, env cldf.Environment, if _, ok := env.BlockChains.EVMChains()[chainSelector]; !ok { return fmt.Errorf("chain not found in environment: %d", chainSelector) } - if chainConfig.Amount == 0 { - return fmt.Errorf("chain %d: amount to withdraw cannot be 0 (zero)", chainSelector) + if chainConfig.Amount == nil || chainConfig.Amount.Cmp(big.NewInt(0)) <= 0 { + return fmt.Errorf("chain %d: amount to withdraw must be positive", chainSelector) } - err := validateEthAddress("payeer", chainConfig.Payeer) - if err != nil { + if err := validateEthAddress("payeer", chainConfig.Payeer); err != nil { return fmt.Errorf("chain %d: %w", chainSelector, err) } - + if common.HexToAddress(chainConfig.Payeer) == (common.Address{}) { + return fmt.Errorf("chain %d: payeer address cannot be zero address", chainSelector) + } } return nil @@ -302,10 +303,12 @@ func ValidateEthBalMonTransferOwnershipConfig(ctx context.Context, env cldf.Envi if _, ok := env.BlockChains.EVMChains()[chainSelector]; !ok { return fmt.Errorf("chain not found in environment: %d", chainSelector) } - err := validateEthAddress("newOwner", chainConfig.NewOwner) - if err != nil { + if err := validateEthAddress("newOwner", chainConfig.NewOwner); err != nil { return fmt.Errorf("chain %d: %w", chainSelector, err) } + if common.HexToAddress(chainConfig.NewOwner) == (common.Address{}) { + return fmt.Errorf("chain %d: newOwner address cannot be zero address", chainSelector) + } } return nil @@ -320,20 +323,23 @@ func ValidateEthBalMonSetWatchListConfig(ctx context.Context, env cldf.Environme if _, ok := env.BlockChains.EVMChains()[chainSelector]; !ok { return fmt.Errorf("chain not found in environment: %d", chainSelector) } - if len(chainConfig.Addresses) == 0 { + n := len(chainConfig.Addresses) + if n == 0 { return fmt.Errorf("chain %d: addresses must not be empty", chainSelector) } - if len(chainConfig.MinBalancesWei) == 0 { - return fmt.Errorf("chain %d: min_balance_wei must not be empty", chainSelector) - } - if len(chainConfig.TopUpAmountsWei) == 0 { - return fmt.Errorf("chain %d: topup_amounts_wei must not be empty", chainSelector) + if len(chainConfig.MinBalancesWei) != n || len(chainConfig.TopUpAmountsWei) != n { + return fmt.Errorf( + "chain %d: addresses, min_balance_wei, and topup_amounts_wei must have the same length (got %d, %d, %d)", + chainSelector, n, len(chainConfig.MinBalancesWei), len(chainConfig.TopUpAmountsWei), + ) } for i, addr := range chainConfig.Addresses { - err := validateEthAddress(fmt.Sprintf("address %d", i), addr.Hex()) - if err != nil { + if err := validateEthAddress(fmt.Sprintf("address %d", i), addr.Hex()); err != nil { return fmt.Errorf("chain %d: %w", chainSelector, err) } + if addr == (common.Address{}) { + return fmt.Errorf("chain %d: address at index %d is zero address", chainSelector, i) + } } } From 27a926745ee995e646b9cf657621f3fc3f5b4967 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Tue, 12 May 2026 17:06:47 -0300 Subject: [PATCH 25/35] leave the inspector as nil and complete on proposalutils functions --- deployment/vault/changeset/ethbalmon_deploy.go | 6 +----- .../changeset/ethbalmon_setKeeperRegistryAddress.go | 10 +--------- deployment/vault/changeset/ethbalmon_setWatchList.go | 10 +--------- .../vault/changeset/ethbalmon_transferOwnership.go | 10 +--------- deployment/vault/changeset/ethbalmon_withdraw.go | 10 +--------- 5 files changed, 5 insertions(+), 41 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy.go b/deployment/vault/changeset/ethbalmon_deploy.go index f24ed58b12a..98edbf97823 100644 --- a/deployment/vault/changeset/ethbalmon_deploy.go +++ b/deployment/vault/changeset/ethbalmon_deploy.go @@ -10,8 +10,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/mcms" - mcmssdk "github.com/smartcontractkit/mcms/sdk" - mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" @@ -422,7 +420,6 @@ func BuildAcceptOwnershipTimelockProposal( var batches []mcmstypes.BatchOperation timelockAddresses := make(map[uint64]string) mcmAddressByChain := make(map[uint64]string) - inspectorPerChain := make(map[uint64]mcmssdk.Inspector) for chainSelector, contractAddr := range input.ContractsByChain { chain, ok := e.BlockChains.EVMChains()[chainSelector] @@ -487,7 +484,6 @@ func BuildAcceptOwnershipTimelockProposal( timelockAddresses[chainSelector] = timelockAddr mcmAddressByChain[chainSelector] = mcmsAddr - inspectorPerChain[chainSelector] = mcmsevmsdk.NewInspector(chain.Client) } description := input.Description @@ -499,7 +495,7 @@ func BuildAcceptOwnershipTimelockProposal( e, timelockAddresses, mcmAddressByChain, - inspectorPerChain, + nil, batches, description, proposalutils.TimelockConfig{ diff --git a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go index 26043f5877d..4de17dc068f 100644 --- a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go +++ b/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go @@ -8,8 +8,6 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/mcms" - mcmssdk "github.com/smartcontractkit/mcms/sdk" - mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" mcmstypes "github.com/smartcontractkit/mcms/types" cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" @@ -100,7 +98,6 @@ var SetKeeperRegistrySequence = operations.NewSequence( var batches []mcmstypes.BatchOperation timelockAddresses := make(map[uint64]string) mcmAddressByChain := make(map[uint64]string) - inspectorPerChain := make(map[uint64]mcmssdk.Inspector) for chainSelector, chainConfig := range input.Chains { opReport, err := operations.ExecuteOperation( @@ -122,14 +119,13 @@ var SetKeeperRegistrySequence = operations.NewSequence( batches = append(batches, opOut.BatchOperation) timelockAddresses[chainSelector] = opOut.TimelockAddress mcmAddressByChain[chainSelector] = opOut.MCMSAddress - inspectorPerChain[chainSelector] = opOut.Inspector } proposal, err := proposalutils.BuildProposalFromBatchesV2( deps.Environment, timelockAddresses, mcmAddressByChain, - inspectorPerChain, + nil, batches, "EthBalMon SetKeeperRegistryAddress", proposalutils.TimelockConfig{MinDelay: 0}, @@ -160,7 +156,6 @@ type SetKeeperRegistryOperationOutput struct { BatchOperation mcmstypes.BatchOperation `json:"batch_operation"` TimelockAddress string `json:"timelock_address"` MCMSAddress string `json:"mcms_address"` - Inspector *mcmsevmsdk.Inspector `json:"inspector"` } var SetKeeperRegistryOperation = operations.NewOperation( @@ -242,8 +237,6 @@ var SetKeeperRegistryOperation = operations.NewOperation( }, } - chainInspector := mcmsevmsdk.NewInspector(chain.Client) - b.Logger.Infow("Generated EthBalMon set keeper registry batch", "chainSelector", input.ChainSelector, "ethBalMon", ethBalMonAddr, @@ -255,7 +248,6 @@ var SetKeeperRegistryOperation = operations.NewOperation( BatchOperation: batch, TimelockAddress: timelockAddr, MCMSAddress: mcmsAddr, - Inspector: chainInspector, }, nil }, ) diff --git a/deployment/vault/changeset/ethbalmon_setWatchList.go b/deployment/vault/changeset/ethbalmon_setWatchList.go index 58aec3042e3..19e6f2370ec 100644 --- a/deployment/vault/changeset/ethbalmon_setWatchList.go +++ b/deployment/vault/changeset/ethbalmon_setWatchList.go @@ -8,8 +8,6 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/mcms" - mcmssdk "github.com/smartcontractkit/mcms/sdk" - mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" mcmstypes "github.com/smartcontractkit/mcms/types" cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" @@ -81,7 +79,6 @@ var EthBalMonSetWatchListSequence = operations.NewSequence( var batches []mcmstypes.BatchOperation timelockAddresses := make(map[uint64]string) mcmAddressByChain := make(map[uint64]string) - inspectorPerChain := make(map[uint64]mcmssdk.Inspector) for chainSelector, chainConfig := range input.Chains { opReport, err := operations.ExecuteOperation(b, EthBalMonSetWatchListOperation, deps, EthBalMonSetWatchListOpInput{ @@ -98,10 +95,9 @@ var EthBalMonSetWatchListSequence = operations.NewSequence( batches = append(batches, opOutput.BatchOperation) timelockAddresses[chainSelector] = opOutput.TimelockAddress mcmAddressByChain[chainSelector] = opOutput.MCMSAddress - inspectorPerChain[chainSelector] = opOutput.Inspector } - proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, inspectorPerChain, batches, "EthBalMon SetWatchList", proposalutils.TimelockConfig{ + proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, nil, batches, "EthBalMon SetWatchList", proposalutils.TimelockConfig{ MinDelay: 0, }) @@ -129,7 +125,6 @@ type EthBalMonSetWatchListOpOutput struct { BatchOperation mcmstypes.BatchOperation `json:"batch_operation"` TimelockAddress string `json:"timelock_address"` MCMSAddress string `json:"mcms_address"` - Inspector *mcmsevmsdk.Inspector `json:"inspector"` } var EthBalMonSetWatchListOperation = operations.NewOperation( @@ -216,8 +211,6 @@ var EthBalMonSetWatchListOperation = operations.NewOperation( }, } - chainInspector := mcmsevmsdk.NewInspector(chain.Client) - b.Logger.Infow("Generated EthBalMon setWatchlist batch", "chainSelector", input.ChainSelector, "ethBalMon", ethBalMonAddr, @@ -229,7 +222,6 @@ var EthBalMonSetWatchListOperation = operations.NewOperation( BatchOperation: batch, TimelockAddress: timelockAddr, MCMSAddress: mcmsAddr, - Inspector: chainInspector, }, nil }, ) diff --git a/deployment/vault/changeset/ethbalmon_transferOwnership.go b/deployment/vault/changeset/ethbalmon_transferOwnership.go index d7c22fd012b..11b008b84e7 100644 --- a/deployment/vault/changeset/ethbalmon_transferOwnership.go +++ b/deployment/vault/changeset/ethbalmon_transferOwnership.go @@ -7,8 +7,6 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/mcms" - mcmssdk "github.com/smartcontractkit/mcms/sdk" - mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" mcmstypes "github.com/smartcontractkit/mcms/types" cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" @@ -78,7 +76,6 @@ var EthBalMonTransferOwnershipSequence = operations.NewSequence( var batches []mcmstypes.BatchOperation timelockAddresses := make(map[uint64]string) mcmAddressByChain := make(map[uint64]string) - inspectorPerChain := make(map[uint64]mcmssdk.Inspector) for chainSelector, chainConfig := range input.Chains { opReport, err := operations.ExecuteOperation(b, EthBalMonTransferOwnershipOperation, deps, EthBalMonTransferOwnershipOpInput{ ChainSelector: chainSelector, @@ -92,10 +89,9 @@ var EthBalMonTransferOwnershipSequence = operations.NewSequence( batches = append(batches, opOutput.BatchOperation) timelockAddresses[chainSelector] = opOutput.TimelockAddress mcmAddressByChain[chainSelector] = opOutput.MCMSAddress - inspectorPerChain[chainSelector] = opOutput.Inspector } - proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, inspectorPerChain, batches, "EthBalMon transferOwnership", proposalutils.TimelockConfig{ + proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, nil, batches, "EthBalMon transferOwnership", proposalutils.TimelockConfig{ MinDelay: 0, }) @@ -121,7 +117,6 @@ type EthBalMonTransferOwnershipOpOutput struct { BatchOperation mcmstypes.BatchOperation `json:"batch_operation"` TimelockAddress string `json:"timelock_address"` MCMSAddress string `json:"mcms_address"` - Inspector *mcmsevmsdk.Inspector `json:"inspector"` } var EthBalMonTransferOwnershipOperation = operations.NewOperation( @@ -195,8 +190,6 @@ var EthBalMonTransferOwnershipOperation = operations.NewOperation( }, } - chainInspector := mcmsevmsdk.NewInspector(chain.Client) - b.Logger.Infow("Generated EthBalMon transferOwnership batch", "chainSelector", input.ChainSelector, "ethBalMon", ethBalMonAddr, @@ -208,7 +201,6 @@ var EthBalMonTransferOwnershipOperation = operations.NewOperation( BatchOperation: batch, TimelockAddress: timelockAddr, MCMSAddress: mcmsAddr, - Inspector: chainInspector, }, nil }, ) diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go index 3d500831416..8b3c7ab8e09 100644 --- a/deployment/vault/changeset/ethbalmon_withdraw.go +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -8,8 +8,6 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/mcms" - mcmssdk "github.com/smartcontractkit/mcms/sdk" - mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" mcmstypes "github.com/smartcontractkit/mcms/types" cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" @@ -79,7 +77,6 @@ var EthBalMonWithdrawSequence = operations.NewSequence( var batches []mcmstypes.BatchOperation timelockAddresses := make(map[uint64]string) mcmAddressByChain := make(map[uint64]string) - inspectorPerChain := make(map[uint64]mcmssdk.Inspector) for chainSelector, chainConfig := range input.Chains { opReport, err := operations.ExecuteOperation(b, EthBalMonWithdrawOperation, deps, EthBalMonWithdrawOpInput{ ChainSelector: chainSelector, @@ -94,10 +91,9 @@ var EthBalMonWithdrawSequence = operations.NewSequence( batches = append(batches, opOutput.BatchOperation) timelockAddresses[chainSelector] = opOutput.TimelockAddress mcmAddressByChain[chainSelector] = opOutput.MCMSAddress - inspectorPerChain[chainSelector] = opOutput.Inspector } - proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, inspectorPerChain, batches, "EthBalMon Withdraw", proposalutils.TimelockConfig{ + proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, nil, batches, "EthBalMon Withdraw", proposalutils.TimelockConfig{ MinDelay: 0, }) @@ -124,7 +120,6 @@ type EthBalMonWithdrawOpOutput struct { BatchOperation mcmstypes.BatchOperation `json:"batch_operation"` TimelockAddress string `json:"timelock_address"` MCMSAddress string `json:"mcms_address"` - Inspector *mcmsevmsdk.Inspector `json:"inspector"` } var EthBalMonWithdrawOperation = operations.NewOperation( @@ -198,8 +193,6 @@ var EthBalMonWithdrawOperation = operations.NewOperation( }, } - chainInspector := mcmsevmsdk.NewInspector(chain.Client) - b.Logger.Infow("Generated EthBalMon withdraw batch", "chainSelector", input.ChainSelector, "ethBalMon", ethBalMonAddr, @@ -212,7 +205,6 @@ var EthBalMonWithdrawOperation = operations.NewOperation( BatchOperation: batch, TimelockAddress: timelockAddr, MCMSAddress: mcmsAddr, - Inspector: chainInspector, }, nil }, ) From 9699421f54de451ed979b66281bd70d12af7c317 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Tue, 12 May 2026 17:15:54 -0300 Subject: [PATCH 26/35] Rename files from camel case to snake --- ...egistryAddress.go => ethbalmon_set_keeper_registry_address.go} | 0 .../{ethbalmon_setWatchList.go => ethbalmon_set_watchlist.go} | 0 ...almon_transferOwnership.go => ethbalmon_transfer_ownership.go} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename deployment/vault/changeset/{ethbalmon_setKeeperRegistryAddress.go => ethbalmon_set_keeper_registry_address.go} (100%) rename deployment/vault/changeset/{ethbalmon_setWatchList.go => ethbalmon_set_watchlist.go} (100%) rename deployment/vault/changeset/{ethbalmon_transferOwnership.go => ethbalmon_transfer_ownership.go} (100%) diff --git a/deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go b/deployment/vault/changeset/ethbalmon_set_keeper_registry_address.go similarity index 100% rename from deployment/vault/changeset/ethbalmon_setKeeperRegistryAddress.go rename to deployment/vault/changeset/ethbalmon_set_keeper_registry_address.go diff --git a/deployment/vault/changeset/ethbalmon_setWatchList.go b/deployment/vault/changeset/ethbalmon_set_watchlist.go similarity index 100% rename from deployment/vault/changeset/ethbalmon_setWatchList.go rename to deployment/vault/changeset/ethbalmon_set_watchlist.go diff --git a/deployment/vault/changeset/ethbalmon_transferOwnership.go b/deployment/vault/changeset/ethbalmon_transfer_ownership.go similarity index 100% rename from deployment/vault/changeset/ethbalmon_transferOwnership.go rename to deployment/vault/changeset/ethbalmon_transfer_ownership.go From 147cc2e035a070d2b045a181b053fb369673beb2 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 13 May 2026 14:59:02 -0300 Subject: [PATCH 27/35] Change action based on mcmc config input --- .../vault/changeset/ethbalmon_deploy.go | 40 ++++++-------- .../vault/changeset/ethbalmon_deploy_test.go | 14 +++-- deployment/vault/changeset/ethbalmon_mcms.go | 55 +++++++++++++++++++ .../ethbalmon_set_keeper_registry_address.go | 16 ++++-- .../changeset/ethbalmon_set_watchlist.go | 22 ++++---- .../changeset/ethbalmon_transfer_ownership.go | 18 +++--- .../vault/changeset/ethbalmon_withdraw.go | 20 ++++--- deployment/vault/changeset/types/types.go | 11 ++++ 8 files changed, 135 insertions(+), 61 deletions(-) create mode 100644 deployment/vault/changeset/ethbalmon_mcms.go diff --git a/deployment/vault/changeset/ethbalmon_deploy.go b/deployment/vault/changeset/ethbalmon_deploy.go index 98edbf97823..77c508347b4 100644 --- a/deployment/vault/changeset/ethbalmon_deploy.go +++ b/deployment/vault/changeset/ethbalmon_deploy.go @@ -74,7 +74,8 @@ func (d deployEthBalMon) Apply(e cldf.Environment, config vaulttypes.DeployEthBa DataStore: e.DataStore, } seqInput := DeployEthBalMonSequenceInput{ - Chains: config.Chains, + Chains: config.Chains, + MCMSConfig: config.MCMSConfig, } seqReport, err := operations.ExecuteSequence(e.OperationsBundle, DeployEthBalMonSequence, deps, seqInput) @@ -131,7 +132,7 @@ func (d deployEthBalMon) Apply(e cldf.Environment, config vaulttypes.DeployEthBa AcceptOwnershipProposalInput{ ContractsByChain: contractsByChain, Description: "Accept ownership of EthBalanceMonitor across chains", - Action: mcmstypes.TimelockActionBypass, + MCMSConfig: deployEthBalMonAcceptOwnershipTimelockConfig(config.MCMSConfig), }, ) if err != nil { @@ -154,7 +155,8 @@ func (d deployEthBalMon) Apply(e cldf.Environment, config vaulttypes.DeployEthBa // ================================================ type DeployEthBalMonSequenceInput struct { - Chains map[uint64]vaulttypes.DeployEthBalMonChainConfig `json:"chains"` + Chains map[uint64]vaulttypes.DeployEthBalMonChainConfig `json:"chains"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type DeployEthBalMonPerChainOutput struct { ChainSelector uint64 @@ -199,7 +201,11 @@ var DeployEthBalMonSequence = operations.NewSequence( if err != nil { return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain %d: failed to get timelock address: %w", chainSelector, err) } - mcmsAddr, err := mustGetContractAddress(deps.DataStore, chainSelector, commontypes.BypasserManyChainMultisig) + mcmsAddr, err := mustGetContractAddress( + deps.DataStore, + chainSelector, + ethBalMonMCMSContractTypeForAction(deployEthBalMonAcceptOwnershipMCMSAction(input.MCMSConfig)), + ) if err != nil { return DeployEthBalMonSequenceOutput{}, fmt.Errorf("chain %d: failed to get mcms address: %w", chainSelector, err) } @@ -406,7 +412,7 @@ var TransferOwnershipOperation = operations.NewOperation( type AcceptOwnershipProposalInput struct { ContractsByChain map[uint64]string Description string - Action mcmstypes.TimelockAction + MCMSConfig proposalutils.TimelockConfig } func BuildAcceptOwnershipTimelockProposal( @@ -436,20 +442,11 @@ func BuildAcceptOwnershipTimelockProposal( return nil, fmt.Errorf("chain %d: %w", chainSelector, err) } - var mcmsAddr string - if input.Action == mcmstypes.TimelockActionBypass { - mcmsAddr, err = mustGetContractAddress( - e.DataStore, - chainSelector, - commontypes.BypasserManyChainMultisig, - ) - } else { - mcmsAddr, err = mustGetContractAddress( - e.DataStore, - chainSelector, - commontypes.ProposerManyChainMultisig, - ) - } + mcmsAddr, err := mustGetContractAddress( + e.DataStore, + chainSelector, + ethBalMonMCMSContractTypeForProposal(&input.MCMSConfig), + ) if err != nil { return nil, fmt.Errorf("chain %d: %w", chainSelector, err) } @@ -491,6 +488,7 @@ func BuildAcceptOwnershipTimelockProposal( description = "Accept ownership of EthBalanceMonitor across chains" } + tlCfg := input.MCMSConfig proposal, err := proposalutils.BuildProposalFromBatchesV2( e, timelockAddresses, @@ -498,9 +496,7 @@ func BuildAcceptOwnershipTimelockProposal( nil, batches, description, - proposalutils.TimelockConfig{ - MinDelay: 0, - }, + tlCfg, ) if err != nil { return nil, fmt.Errorf("failed to build timelock proposal: %w", err) diff --git a/deployment/vault/changeset/ethbalmon_deploy_test.go b/deployment/vault/changeset/ethbalmon_deploy_test.go index 5ab42afdf11..0bae3289385 100644 --- a/deployment/vault/changeset/ethbalmon_deploy_test.go +++ b/deployment/vault/changeset/ethbalmon_deploy_test.go @@ -19,6 +19,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" ) @@ -142,7 +143,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { _, err = BuildAcceptOwnershipTimelockProposal(*env, AcceptOwnershipProposalInput{ ContractsByChain: map[uint64]string{}, Description: "test", - Action: mcmstypes.TimelockActionBypass, + MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, }) require.Error(t, err) require.Contains(t, err.Error(), "no contracts provided") @@ -163,7 +164,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { otherSel: testAddr1, }, Description: "test", - Action: mcmstypes.TimelockActionBypass, + MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, }) require.Error(t, err) require.Contains(t, err.Error(), "not found in environment") @@ -183,7 +184,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { selector: testAddr1, }, Description: "test", - Action: mcmstypes.TimelockActionBypass, + MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, }) require.Error(t, err) }) @@ -219,7 +220,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { prop, err := BuildAcceptOwnershipTimelockProposal(rt.Environment(), AcceptOwnershipProposalInput{ ContractsByChain: contractsByChain, Description: customDesc, - Action: mcmstypes.TimelockActionBypass, + MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, }) require.NoError(t, err) require.NotNil(t, prop) @@ -253,7 +254,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { prop, err := BuildAcceptOwnershipTimelockProposal(rt.Environment(), AcceptOwnershipProposalInput{ ContractsByChain: map[uint64]string{selector: addr}, Description: "", - Action: mcmstypes.TimelockActionBypass, + MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, }) require.NoError(t, err) require.Equal(t, "Accept ownership of EthBalanceMonitor across chains", prop.Description) @@ -562,7 +563,8 @@ func assertEthBalMonDeployOutput( for sel, chainCfg := range cfg.Chains { timelockAddr, err := GetContractAddress(env.DataStore, sel, commontypes.RBACTimelock) require.NoError(t, err) - mcmsAddr, err := GetContractAddress(env.DataStore, sel, commontypes.BypasserManyChainMultisig) + mcmsType := ethBalMonMCMSContractTypeForAction(deployEthBalMonAcceptOwnershipMCMSAction(cfg.MCMSConfig)) + mcmsAddr, err := GetContractAddress(env.DataStore, sel, mcmsType) require.NoError(t, err) meta, ok := bySel[sel] diff --git a/deployment/vault/changeset/ethbalmon_mcms.go b/deployment/vault/changeset/ethbalmon_mcms.go new file mode 100644 index 00000000000..25dbc7f40b9 --- /dev/null +++ b/deployment/vault/changeset/ethbalmon_mcms.go @@ -0,0 +1,55 @@ +package changeset + +import ( + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + mcmstypes "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" +) + +// ethBalMonMCMSContractTypeForAction selects the MCMS contract type in the datastore for EthBalMon +// timelock proposals, matching operations.generateMCMSProposals. +func ethBalMonMCMSContractTypeForAction(action mcmstypes.TimelockAction) cldf.ContractType { + if action == mcmstypes.TimelockActionBypass { + return commontypes.BypasserManyChainMultisig + } + return commontypes.ProposerManyChainMultisig +} + +// ethBalMonMCMSContractTypeForProposal resolves MCMS datastore type from optional timelock config. +// When cfg is nil or MCMSAction is empty, the action is treated as schedule (non-bypass), so the proposer MCM is used. +func ethBalMonMCMSContractTypeForProposal(cfg *proposalutils.TimelockConfig) cldf.ContractType { + action := mcmstypes.TimelockActionSchedule + if cfg != nil && cfg.MCMSAction != "" { + action = cfg.MCMSAction + } + return ethBalMonMCMSContractTypeForAction(action) +} + +func ethBalMonProposalTimelockConfig(cfg *proposalutils.TimelockConfig) proposalutils.TimelockConfig { + if cfg == nil { + return proposalutils.TimelockConfig{MinDelay: 0} + } + return *cfg +} + +// deployEthBalMonAcceptOwnershipMCMSAction returns the MCMS timelock action for the post-deploy accept-ownership proposal. +// When cfg is nil or MCMSAction is unset, the deploy flow defaults to bypass. +func deployEthBalMonAcceptOwnershipMCMSAction(cfg *proposalutils.TimelockConfig) mcmstypes.TimelockAction { + if cfg == nil || cfg.MCMSAction == "" { + return mcmstypes.TimelockActionBypass + } + return cfg.MCMSAction +} + +func deployEthBalMonAcceptOwnershipTimelockConfig(cfg *proposalutils.TimelockConfig) proposalutils.TimelockConfig { + out := proposalutils.TimelockConfig{MinDelay: 0} + if cfg != nil { + out = *cfg + } + if out.MCMSAction == "" { + out.MCMSAction = mcmstypes.TimelockActionBypass + } + return out +} diff --git a/deployment/vault/changeset/ethbalmon_set_keeper_registry_address.go b/deployment/vault/changeset/ethbalmon_set_keeper_registry_address.go index 4de17dc068f..bf91f8ba6bd 100644 --- a/deployment/vault/changeset/ethbalmon_set_keeper_registry_address.go +++ b/deployment/vault/changeset/ethbalmon_set_keeper_registry_address.go @@ -52,7 +52,8 @@ func (sk setKeeperRegistryAddress) Apply( } seqInput := EthBalMonSetKeeperRegistryAddressSequenceInput{ - Chains: config.Chains, + Chains: config.Chains, + MCMSConfig: config.MCMSConfig, } seqReport, err := operations.ExecuteSequence( @@ -71,7 +72,8 @@ func (sk setKeeperRegistryAddress) Apply( } type EthBalMonSetKeeperRegistryAddressSequenceInput struct { - Chains map[uint64]vaulttypes.SetKeeperRegistryChainConfig `json:"chains"` + Chains map[uint64]vaulttypes.SetKeeperRegistryChainConfig `json:"chains"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonSetKeeperRegistryAddressSequenceOutput struct { @@ -107,6 +109,7 @@ var SetKeeperRegistrySequence = operations.NewSequence( SetKeeperRegistryOperationInput{ ChainSelector: chainSelector, NewKeeperRegistryAddress: chainConfig.NewKeeperRegistryAddress, + MCMSConfig: input.MCMSConfig, }, ) if err != nil { @@ -128,7 +131,7 @@ var SetKeeperRegistrySequence = operations.NewSequence( nil, batches, "EthBalMon SetKeeperRegistryAddress", - proposalutils.TimelockConfig{MinDelay: 0}, + ethBalMonProposalTimelockConfig(input.MCMSConfig), ) if err != nil { return EthBalMonSetKeeperRegistryAddressSequenceOutput{}, @@ -147,8 +150,9 @@ var SetKeeperRegistrySequence = operations.NewSequence( ) type SetKeeperRegistryOperationInput struct { - ChainSelector uint64 `json:"chain_selector"` - NewKeeperRegistryAddress string `json:"new_keeper_registry_address"` + ChainSelector uint64 `json:"chain_selector"` + NewKeeperRegistryAddress string `json:"new_keeper_registry_address"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type SetKeeperRegistryOperationOutput struct { @@ -195,7 +199,7 @@ var SetKeeperRegistryOperation = operations.NewOperation( mcmsAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - commontypes.BypasserManyChainMultisig, + ethBalMonMCMSContractTypeForProposal(input.MCMSConfig), ) if err != nil { return SetKeeperRegistryOperationOutput{}, diff --git a/deployment/vault/changeset/ethbalmon_set_watchlist.go b/deployment/vault/changeset/ethbalmon_set_watchlist.go index 19e6f2370ec..7450e2bab16 100644 --- a/deployment/vault/changeset/ethbalmon_set_watchlist.go +++ b/deployment/vault/changeset/ethbalmon_set_watchlist.go @@ -47,7 +47,8 @@ func (sw ethBalMonSetWatchList) Apply(e cldf.Environment, config vaulttypes.EthB } seqInput := EthBalMonSetWatchListSeqInput{ - Chains: config.Chains, + Chains: config.Chains, + MCMSConfig: config.MCMSConfig, } seqReport, err := operations.ExecuteSequence(e.OperationsBundle, EthBalMonSetWatchListSequence, deps, seqInput) @@ -61,7 +62,8 @@ func (sw ethBalMonSetWatchList) Apply(e cldf.Environment, config vaulttypes.EthB } type EthBalMonSetWatchListSeqInput struct { - Chains map[uint64]vaulttypes.EthBalMonSetWatchListChainConfig `json:"chains"` + Chains map[uint64]vaulttypes.EthBalMonSetWatchListChainConfig `json:"chains"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonSetWatchListSeqOutput struct { @@ -86,6 +88,7 @@ var EthBalMonSetWatchListSequence = operations.NewSequence( Addresses: chainConfig.Addresses, MinBalancesWei: chainConfig.MinBalancesWei, TopUpAmountsWei: chainConfig.TopUpAmountsWei, + MCMSConfig: input.MCMSConfig, }) if err != nil { return EthBalMonSetWatchListSeqOutput{}, fmt.Errorf("chain %d: failed to generate setWatchList batch: %w", chainSelector, err) @@ -97,9 +100,7 @@ var EthBalMonSetWatchListSequence = operations.NewSequence( mcmAddressByChain[chainSelector] = opOutput.MCMSAddress } - proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, nil, batches, "EthBalMon SetWatchList", proposalutils.TimelockConfig{ - MinDelay: 0, - }) + proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, nil, batches, "EthBalMon SetWatchList", ethBalMonProposalTimelockConfig(input.MCMSConfig)) if err != nil { return EthBalMonSetWatchListSeqOutput{}, fmt.Errorf("failed to build timelock proposal: %w", err) @@ -114,10 +115,11 @@ var EthBalMonSetWatchListSequence = operations.NewSequence( ) type EthBalMonSetWatchListOpInput struct { - ChainSelector uint64 `json:"chain_selector"` - Addresses []common.Address `json:"addresses"` - MinBalancesWei []big.Int `json:"min_balance_wei"` - TopUpAmountsWei []big.Int `json:"topup_amounts_wei"` + ChainSelector uint64 `json:"chain_selector"` + Addresses []common.Address `json:"addresses"` + MinBalancesWei []big.Int `json:"min_balance_wei"` + TopUpAmountsWei []big.Int `json:"topup_amounts_wei"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonSetWatchListOpOutput struct { @@ -164,7 +166,7 @@ var EthBalMonSetWatchListOperation = operations.NewOperation( mcmsAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - commontypes.BypasserManyChainMultisig, + ethBalMonMCMSContractTypeForProposal(input.MCMSConfig), ) if err != nil { return EthBalMonSetWatchListOpOutput{}, diff --git a/deployment/vault/changeset/ethbalmon_transfer_ownership.go b/deployment/vault/changeset/ethbalmon_transfer_ownership.go index 11b008b84e7..63cd5c0862e 100644 --- a/deployment/vault/changeset/ethbalmon_transfer_ownership.go +++ b/deployment/vault/changeset/ethbalmon_transfer_ownership.go @@ -45,7 +45,8 @@ func (tw ethBalMonTransferOwnership) Apply(e cldf.Environment, config vaulttypes DataStore: e.DataStore, } seqInput := EthBalMonTransferOwnershipSeqInput{ - Chains: config.Chains, + Chains: config.Chains, + MCMSConfig: config.MCMSConfig, } seqReport, err := operations.ExecuteSequence(e.OperationsBundle, EthBalMonTransferOwnershipSequence, deps, seqInput) if err != nil { @@ -58,7 +59,8 @@ func (tw ethBalMonTransferOwnership) Apply(e cldf.Environment, config vaulttypes } type EthBalMonTransferOwnershipSeqInput struct { - Chains map[uint64]vaulttypes.EthBalMonTransferOwnershipChainConfig `json:"chains"` + Chains map[uint64]vaulttypes.EthBalMonTransferOwnershipChainConfig `json:"chains"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonTransferOwnershipSeqOutput struct { @@ -80,6 +82,7 @@ var EthBalMonTransferOwnershipSequence = operations.NewSequence( opReport, err := operations.ExecuteOperation(b, EthBalMonTransferOwnershipOperation, deps, EthBalMonTransferOwnershipOpInput{ ChainSelector: chainSelector, NewOwner: chainConfig.NewOwner, + MCMSConfig: input.MCMSConfig, }) if err != nil { return EthBalMonTransferOwnershipSeqOutput{}, fmt.Errorf("chain %d: failed to generate ownership batch: %w", chainSelector, err) @@ -91,9 +94,7 @@ var EthBalMonTransferOwnershipSequence = operations.NewSequence( mcmAddressByChain[chainSelector] = opOutput.MCMSAddress } - proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, nil, batches, "EthBalMon transferOwnership", proposalutils.TimelockConfig{ - MinDelay: 0, - }) + proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, nil, batches, "EthBalMon transferOwnership", ethBalMonProposalTimelockConfig(input.MCMSConfig)) if err != nil { return EthBalMonTransferOwnershipSeqOutput{}, fmt.Errorf("failed to build timelock proposal: %w", err) @@ -108,8 +109,9 @@ var EthBalMonTransferOwnershipSequence = operations.NewSequence( ) type EthBalMonTransferOwnershipOpInput struct { - ChainSelector uint64 `json:"chain_selector"` - NewOwner string `json:"new_owner"` + ChainSelector uint64 `json:"chain_selector"` + NewOwner string `json:"new_owner"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonTransferOwnershipOpOutput struct { @@ -156,7 +158,7 @@ var EthBalMonTransferOwnershipOperation = operations.NewOperation( mcmsAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - commontypes.BypasserManyChainMultisig, + ethBalMonMCMSContractTypeForProposal(input.MCMSConfig), ) if err != nil { return EthBalMonTransferOwnershipOpOutput{}, diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go index 8b3c7ab8e09..4ed4f8abcf8 100644 --- a/deployment/vault/changeset/ethbalmon_withdraw.go +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -46,7 +46,8 @@ func (w ethBalMonWithdraw) Apply(e cldf.Environment, config vaulttypes.EthBalMon DataStore: e.DataStore, } seqInput := EthBalMonWithdrawSeqInput{ - Chains: config.Chains, + Chains: config.Chains, + MCMSConfig: config.MCMSConfig, } seqReport, err := operations.ExecuteSequence(e.OperationsBundle, EthBalMonWithdrawSequence, deps, seqInput) if err != nil { @@ -59,7 +60,8 @@ func (w ethBalMonWithdraw) Apply(e cldf.Environment, config vaulttypes.EthBalMon } type EthBalMonWithdrawSeqInput struct { - Chains map[uint64]vaulttypes.EthBalMonWithdrawChainConfig `json:"chains"` + Chains map[uint64]vaulttypes.EthBalMonWithdrawChainConfig `json:"chains"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonWithdrawSeqOutput struct { @@ -82,6 +84,7 @@ var EthBalMonWithdrawSequence = operations.NewSequence( ChainSelector: chainSelector, Amount: chainConfig.Amount, Payeer: chainConfig.Payeer, + MCMSConfig: input.MCMSConfig, }) if err != nil { return EthBalMonWithdrawSeqOutput{}, fmt.Errorf("chain %d: failed to generate withdraw batch: %w", chainSelector, err) @@ -93,9 +96,7 @@ var EthBalMonWithdrawSequence = operations.NewSequence( mcmAddressByChain[chainSelector] = opOutput.MCMSAddress } - proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, nil, batches, "EthBalMon Withdraw", proposalutils.TimelockConfig{ - MinDelay: 0, - }) + proposal, err := proposalutils.BuildProposalFromBatchesV2(deps.Environment, timelockAddresses, mcmAddressByChain, nil, batches, "EthBalMon Withdraw", ethBalMonProposalTimelockConfig(input.MCMSConfig)) if err != nil { return EthBalMonWithdrawSeqOutput{}, fmt.Errorf("failed to build timelock proposal: %w", err) @@ -110,9 +111,10 @@ var EthBalMonWithdrawSequence = operations.NewSequence( ) type EthBalMonWithdrawOpInput struct { - ChainSelector uint64 `json:"chain_selector"` - Amount *big.Int `json:"amount"` - Payeer string `json:"payeer"` + ChainSelector uint64 `json:"chain_selector"` + Amount *big.Int `json:"amount"` + Payeer string `json:"payeer"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonWithdrawOpOutput struct { @@ -159,7 +161,7 @@ var EthBalMonWithdrawOperation = operations.NewOperation( mcmsAddr, err := mustGetContractAddress( deps.DataStore, input.ChainSelector, - commontypes.BypasserManyChainMultisig, + ethBalMonMCMSContractTypeForProposal(input.MCMSConfig), ) if err != nil { return EthBalMonWithdrawOpOutput{}, diff --git a/deployment/vault/changeset/types/types.go b/deployment/vault/changeset/types/types.go index abb7b69df79..4c841bd8e96 100644 --- a/deployment/vault/changeset/types/types.go +++ b/deployment/vault/changeset/types/types.go @@ -89,6 +89,9 @@ type DeployEthBalMonChainConfig struct { // Keys are chain selectors; each value configures keeper/registry wiring and min wait for that chain. type DeployEthBalMonInput struct { Chains map[uint64]DeployEthBalMonChainConfig `json:"chains"` + // MCMSConfig optionally configures the post-deploy accept-ownership timelock proposal (MCMS action, delay, etc.). + // When nil or MCMSAction is unset, the deploy flow defaults accept-ownership to bypass (historical behavior). + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } // EthBalMonContractType is the datastore / MCMS contract type label for EthBalMon deployments. @@ -104,6 +107,8 @@ type SetKeeperRegistryChainConfig struct { // Keys are chain selectors with the registry address to set on each chain's EthBalMon. type EthBalMonSetKeeperRegistryAddressInput struct { Chains map[uint64]SetKeeperRegistryChainConfig `json:"chains"` + // MCMSConfig optionally configures the timelock proposal; when nil, schedule + proposer MCM is used. + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } // EthBalMonSetWatchListChainConfig replaces the monitored addresses and thresholds on one chain. @@ -118,6 +123,8 @@ type EthBalMonSetWatchListChainConfig struct { // Keys are chain selectors; each value is the full watch list to install on that chain's EthBalMon. type EthBalMonSetWatchListInput struct { Chains map[uint64]EthBalMonSetWatchListChainConfig `json:"chains"` + // MCMSConfig optionally configures the timelock proposal; when nil, schedule + proposer MCM is used. + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } // EthBalMonWithdrawChainConfig configures a native-token withdraw from EthBalMon on one chain. @@ -132,6 +139,8 @@ type EthBalMonWithdrawChainConfig struct { // Keys are chain selectors; each value specifies amount and recipient for that chain. type EthBalMonWithdrawInput struct { Chains map[uint64]EthBalMonWithdrawChainConfig `json:"chains"` + // MCMSConfig optionally configures the timelock proposal; when nil, schedule + proposer MCM is used. + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } // EthBalMonTransferOwnershipChainConfig sets the new owner of EthBalMon on one chain. @@ -144,4 +153,6 @@ type EthBalMonTransferOwnershipChainConfig struct { // Keys are chain selectors; each value is the new owner for that chain's EthBalMon instance. type EthBalMonTransferOwnershipInput struct { Chains map[uint64]EthBalMonTransferOwnershipChainConfig `json:"chains"` + // MCMSConfig optionally configures the timelock proposal; when nil, schedule + proposer MCM is used. + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } From 4cbd0395d1c5645005a1bf667f85e79c19dd4dfd Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 13 May 2026 15:27:23 -0300 Subject: [PATCH 28/35] Fix lint --- deployment/vault/changeset/ethbalmon_deploy.go | 10 ++++------ deployment/vault/changeset/ethbalmon_mcms.go | 3 ++- .../changeset/ethbalmon_set_keeper_registry_address.go | 8 ++++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy.go b/deployment/vault/changeset/ethbalmon_deploy.go index 77c508347b4..ff94eb2dda2 100644 --- a/deployment/vault/changeset/ethbalmon_deploy.go +++ b/deployment/vault/changeset/ethbalmon_deploy.go @@ -8,17 +8,15 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/mcms" + mcmstypes "github.com/smartcontractkit/mcms/types" cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + ds "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/eth_balance_monitor_wrapper" - mcmstypes "github.com/smartcontractkit/mcms/types" - - ds "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-deployments-framework/operations" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" vaulttypes "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" @@ -156,7 +154,7 @@ func (d deployEthBalMon) Apply(e cldf.Environment, config vaulttypes.DeployEthBa type DeployEthBalMonSequenceInput struct { Chains map[uint64]vaulttypes.DeployEthBalMonChainConfig `json:"chains"` - MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type DeployEthBalMonPerChainOutput struct { ChainSelector uint64 diff --git a/deployment/vault/changeset/ethbalmon_mcms.go b/deployment/vault/changeset/ethbalmon_mcms.go index 25dbc7f40b9..714970526f6 100644 --- a/deployment/vault/changeset/ethbalmon_mcms.go +++ b/deployment/vault/changeset/ethbalmon_mcms.go @@ -1,9 +1,10 @@ package changeset import ( - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" mcmstypes "github.com/smartcontractkit/mcms/types" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" ) diff --git a/deployment/vault/changeset/ethbalmon_set_keeper_registry_address.go b/deployment/vault/changeset/ethbalmon_set_keeper_registry_address.go index bf91f8ba6bd..f47d3fcf009 100644 --- a/deployment/vault/changeset/ethbalmon_set_keeper_registry_address.go +++ b/deployment/vault/changeset/ethbalmon_set_keeper_registry_address.go @@ -73,7 +73,7 @@ func (sk setKeeperRegistryAddress) Apply( type EthBalMonSetKeeperRegistryAddressSequenceInput struct { Chains map[uint64]vaulttypes.SetKeeperRegistryChainConfig `json:"chains"` - MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonSetKeeperRegistryAddressSequenceOutput struct { @@ -150,9 +150,9 @@ var SetKeeperRegistrySequence = operations.NewSequence( ) type SetKeeperRegistryOperationInput struct { - ChainSelector uint64 `json:"chain_selector"` - NewKeeperRegistryAddress string `json:"new_keeper_registry_address"` - MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` + ChainSelector uint64 `json:"chain_selector"` + NewKeeperRegistryAddress string `json:"new_keeper_registry_address"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type SetKeeperRegistryOperationOutput struct { From 68cb16bd35e319824a86b2bd1c42760762686b26 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 13 May 2026 15:36:52 -0300 Subject: [PATCH 29/35] Format structures --- .../vault/changeset/ethbalmon_set_watchlist.go | 12 ++++++------ .../vault/changeset/ethbalmon_transfer_ownership.go | 8 ++++---- deployment/vault/changeset/ethbalmon_withdraw.go | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_set_watchlist.go b/deployment/vault/changeset/ethbalmon_set_watchlist.go index 7450e2bab16..7415aa4503e 100644 --- a/deployment/vault/changeset/ethbalmon_set_watchlist.go +++ b/deployment/vault/changeset/ethbalmon_set_watchlist.go @@ -63,7 +63,7 @@ func (sw ethBalMonSetWatchList) Apply(e cldf.Environment, config vaulttypes.EthB type EthBalMonSetWatchListSeqInput struct { Chains map[uint64]vaulttypes.EthBalMonSetWatchListChainConfig `json:"chains"` - MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonSetWatchListSeqOutput struct { @@ -115,11 +115,11 @@ var EthBalMonSetWatchListSequence = operations.NewSequence( ) type EthBalMonSetWatchListOpInput struct { - ChainSelector uint64 `json:"chain_selector"` - Addresses []common.Address `json:"addresses"` - MinBalancesWei []big.Int `json:"min_balance_wei"` - TopUpAmountsWei []big.Int `json:"topup_amounts_wei"` - MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` + ChainSelector uint64 `json:"chain_selector"` + Addresses []common.Address `json:"addresses"` + MinBalancesWei []big.Int `json:"min_balance_wei"` + TopUpAmountsWei []big.Int `json:"topup_amounts_wei"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonSetWatchListOpOutput struct { diff --git a/deployment/vault/changeset/ethbalmon_transfer_ownership.go b/deployment/vault/changeset/ethbalmon_transfer_ownership.go index 63cd5c0862e..a188c916695 100644 --- a/deployment/vault/changeset/ethbalmon_transfer_ownership.go +++ b/deployment/vault/changeset/ethbalmon_transfer_ownership.go @@ -60,7 +60,7 @@ func (tw ethBalMonTransferOwnership) Apply(e cldf.Environment, config vaulttypes type EthBalMonTransferOwnershipSeqInput struct { Chains map[uint64]vaulttypes.EthBalMonTransferOwnershipChainConfig `json:"chains"` - MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonTransferOwnershipSeqOutput struct { @@ -109,9 +109,9 @@ var EthBalMonTransferOwnershipSequence = operations.NewSequence( ) type EthBalMonTransferOwnershipOpInput struct { - ChainSelector uint64 `json:"chain_selector"` - NewOwner string `json:"new_owner"` - MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` + ChainSelector uint64 `json:"chain_selector"` + NewOwner string `json:"new_owner"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonTransferOwnershipOpOutput struct { diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go index 4ed4f8abcf8..5edd89532f9 100644 --- a/deployment/vault/changeset/ethbalmon_withdraw.go +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -61,7 +61,7 @@ func (w ethBalMonWithdraw) Apply(e cldf.Environment, config vaulttypes.EthBalMon type EthBalMonWithdrawSeqInput struct { Chains map[uint64]vaulttypes.EthBalMonWithdrawChainConfig `json:"chains"` - MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonWithdrawSeqOutput struct { @@ -111,10 +111,10 @@ var EthBalMonWithdrawSequence = operations.NewSequence( ) type EthBalMonWithdrawOpInput struct { - ChainSelector uint64 `json:"chain_selector"` - Amount *big.Int `json:"amount"` - Payeer string `json:"payeer"` - MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` + ChainSelector uint64 `json:"chain_selector"` + Amount *big.Int `json:"amount"` + Payeer string `json:"payeer"` + MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } type EthBalMonWithdrawOpOutput struct { From 57663e6e5063ac329d0aa23519a1f27903a54adb Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 13 May 2026 16:15:06 -0300 Subject: [PATCH 30/35] Format file --- deployment/vault/changeset/ethbalmon_deploy_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy_test.go b/deployment/vault/changeset/ethbalmon_deploy_test.go index 0bae3289385..9922b9a9044 100644 --- a/deployment/vault/changeset/ethbalmon_deploy_test.go +++ b/deployment/vault/changeset/ethbalmon_deploy_test.go @@ -143,7 +143,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { _, err = BuildAcceptOwnershipTimelockProposal(*env, AcceptOwnershipProposalInput{ ContractsByChain: map[uint64]string{}, Description: "test", - MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, + MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, }) require.Error(t, err) require.Contains(t, err.Error(), "no contracts provided") @@ -164,7 +164,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { otherSel: testAddr1, }, Description: "test", - MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, + MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, }) require.Error(t, err) require.Contains(t, err.Error(), "not found in environment") @@ -184,7 +184,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { selector: testAddr1, }, Description: "test", - MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, + MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, }) require.Error(t, err) }) @@ -220,7 +220,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { prop, err := BuildAcceptOwnershipTimelockProposal(rt.Environment(), AcceptOwnershipProposalInput{ ContractsByChain: contractsByChain, Description: customDesc, - MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, + MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, }) require.NoError(t, err) require.NotNil(t, prop) @@ -254,7 +254,7 @@ func TestBuildAcceptOwnershipTimelockProposal(t *testing.T) { prop, err := BuildAcceptOwnershipTimelockProposal(rt.Environment(), AcceptOwnershipProposalInput{ ContractsByChain: map[uint64]string{selector: addr}, Description: "", - MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, + MCMSConfig: proposalutils.TimelockConfig{MinDelay: 0, MCMSAction: mcmstypes.TimelockActionBypass}, }) require.NoError(t, err) require.Equal(t, "Accept ownership of EthBalanceMonitor across chains", prop.Description) From 7933f542c248ddcb5bed4f9afa1b48c38f49a428 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 13 May 2026 16:52:43 -0300 Subject: [PATCH 31/35] Validate balance >0 --- deployment/vault/changeset/validation.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index b3b6096cde5..5d5d14aac88 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -340,6 +340,13 @@ func ValidateEthBalMonSetWatchListConfig(ctx context.Context, env cldf.Environme if addr == (common.Address{}) { return fmt.Errorf("chain %d: address at index %d is zero address", chainSelector, i) } + // Check MinBalancesWei and TopUpAmountsWei are >= 0 + if chainConfig.MinBalancesWei[i].Cmp(big.NewInt(0)) < 0 { + return fmt.Errorf("chain %d: min_balance_wei at index %d must be >= 0", chainSelector, i) + } + if chainConfig.TopUpAmountsWei[i].Cmp(big.NewInt(0)) < 0 { + return fmt.Errorf("chain %d: topup_amounts_wei at index %d must be >= 0", chainSelector, i) + } } } From ee1d25f4b93e9dbb293fc75de8afd5fd3a47af14 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 13 May 2026 16:57:24 -0300 Subject: [PATCH 32/35] Validate MCMS is on datastore --- .../vault/changeset/ethbalmon_deploy_test.go | 38 ++++++++++++++++--- deployment/vault/changeset/validation.go | 36 ++++++++++++++++++ 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy_test.go b/deployment/vault/changeset/ethbalmon_deploy_test.go index 9922b9a9044..d66a23b7087 100644 --- a/deployment/vault/changeset/ethbalmon_deploy_test.go +++ b/deployment/vault/changeset/ethbalmon_deploy_test.go @@ -36,10 +36,11 @@ func TestDeployEthBalMonValidation(t *testing.T) { require.NoError(t, err) tests := []struct { - name string - config types.DeployEthBalMonInput - wantError bool - errorMsg string + name string + config types.DeployEthBalMonInput + wantError bool + errorMsg string + setupMCMSIn bool }{ { name: "empty chains", @@ -97,6 +98,18 @@ func TestDeployEthBalMonValidation(t *testing.T) { wantError: true, errorMsg: fmt.Sprintf("chain %d: setKeeperRegistryAddress is not a valid hex address: not-a-valid-address", selector), }, + { + name: "missing MCMS and timelock in datastore", + config: types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: { + SetKeeperRegistryAddress: "0x1234567890123456789012345678901234567890", + }, + }, + }, + wantError: true, + errorMsg: "failed to get addresses from datastore", + }, { name: "valid config", config: types.DeployEthBalMonInput{ @@ -106,7 +119,8 @@ func TestDeployEthBalMonValidation(t *testing.T) { }, }, }, - wantError: false, + wantError: false, + setupMCMSIn: true, }, } @@ -114,7 +128,19 @@ func TestDeployEthBalMonValidation(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - err := ValidateDeployEthBalMonConfig(env.GetContext(), *env, test.config) + var testEnv cldf.Environment + if test.setupMCMSIn { + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + setupMCMSInfrastructure(t, rt, []uint64{selector}) + testEnv = rt.Environment() + } else { + testEnv = *env + } + + err := ValidateDeployEthBalMonConfig(testEnv.GetContext(), testEnv, test.config) if test.wantError { require.Error(t, err) diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index 5d5d14aac88..aa4a5e669f8 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -237,6 +237,10 @@ func ValidateDeployEthBalMonConfig(ctx context.Context, env cldf.Environment, cf return errors.New("chains must not be empty") } + if cfg.MCMSConfig != nil && cfg.MCMSConfig.MinDelay < 0 { + return fmt.Errorf("MCMS minimum delay cannot be negative: %d", cfg.MCMSConfig.MinDelay) + } + for chainSelector, chainCfg := range cfg.Chains { if err := validateChainSelector(chainSelector, env); err != nil { return fmt.Errorf("chain %d: %w", chainSelector, err) @@ -247,11 +251,43 @@ func ValidateDeployEthBalMonConfig(ctx context.Context, env cldf.Environment, cf if err := validateEthAddress("setKeeperRegistryAddress", chainCfg.SetKeeperRegistryAddress); err != nil { return fmt.Errorf("chain %d: %w", chainSelector, err) } + if err := validateDeployEthBalMonMCMSInDatastore(env, chainSelector, cfg.MCMSConfig); err != nil { + return fmt.Errorf("chain %d: %w", chainSelector, err) + } } return nil } +// validateDeployEthBalMonMCMSInDatastore ensures RBACTimelock, the MCM used for the post-deploy +// accept-ownership proposal (bypasser vs proposer per cfg.MCMSConfig), and loadable MCMS state +// exist in the datastore — matching DeployEthBalMonSequence and BuildAcceptOwnershipTimelockProposal. +func validateDeployEthBalMonMCMSInDatastore(e cldf.Environment, chainSelector uint64, mcmsCfg *proposalutils.TimelockConfig) error { + const emptyQualifier = "" + addresses, err := state.GetAddressTypeVersionByQualifier(e.DataStore.Addresses(), chainSelector, emptyQualifier) + if err != nil { + return fmt.Errorf("failed to get addresses from datastore: %w", err) + } + + _, err = GetContractAddress(e.DataStore, chainSelector, commontypes.RBACTimelock) + if err != nil { + return fmt.Errorf("timelock not found in datastore: %w", err) + } + + mcmType := ethBalMonMCMSContractTypeForAction(deployEthBalMonAcceptOwnershipMCMSAction(mcmsCfg)) + _, err = GetContractAddress(e.DataStore, chainSelector, mcmType) + if err != nil { + return fmt.Errorf("MCMS (%s) not found in datastore: %w", mcmType, err) + } + + chain := e.BlockChains.EVMChains()[chainSelector] + _, err = changeset.MaybeLoadMCMSWithTimelockChainState(chain, addresses) + if err != nil { + return fmt.Errorf("failed to load MCMS with timelock state: %w", err) + } + return nil +} + func ValidateSetKeeperRegistryAddressConfig(ctx context.Context, env cldf.Environment, cfg types.EthBalMonSetKeeperRegistryAddressInput) error { if len(cfg.Chains) == 0 { return errors.New("no chains provided") From 558faf68a5d5ef9bea95a1aaf33036362921b6d8 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 13 May 2026 17:22:32 -0300 Subject: [PATCH 33/35] Add zero address check after check if is valid hex --- deployment/vault/changeset/validation.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index aa4a5e669f8..011d7cc8e78 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -298,8 +298,11 @@ func ValidateSetKeeperRegistryAddressConfig(ctx context.Context, env cldf.Enviro return fmt.Errorf("chain not found in environment: %d", chainSelector) } - if !common.IsHexAddress(chainConfig.NewKeeperRegistryAddress) { - return fmt.Errorf("chain %d: invalid keeper registry address: %s", chainSelector, chainConfig.NewKeeperRegistryAddress) + if err := validateEthAddress("new_keeper_registry_address", chainConfig.NewKeeperRegistryAddress); err != nil { + return fmt.Errorf("chain %d: %w", chainSelector, err) + } + if common.HexToAddress(chainConfig.NewKeeperRegistryAddress) == (common.Address{}) { + return fmt.Errorf("chain %d: keeper registry address cannot be zero address", chainSelector) } } From 0db1992cff3f14381552627e139f4a06c6321ee5 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Wed, 13 May 2026 17:28:33 -0300 Subject: [PATCH 34/35] Format file --- deployment/vault/changeset/ethbalmon_deploy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/vault/changeset/ethbalmon_deploy_test.go b/deployment/vault/changeset/ethbalmon_deploy_test.go index d66a23b7087..ef29c07b9cb 100644 --- a/deployment/vault/changeset/ethbalmon_deploy_test.go +++ b/deployment/vault/changeset/ethbalmon_deploy_test.go @@ -119,7 +119,7 @@ func TestDeployEthBalMonValidation(t *testing.T) { }, }, }, - wantError: false, + wantError: false, setupMCMSIn: true, }, } From 730d65dde054a582cb7946fa465ceea0e13bb999 Mon Sep 17 00:00:00 2001 From: bitrider23 Date: Thu, 14 May 2026 16:51:01 -0300 Subject: [PATCH 35/35] Fix copilot feedback --- .../vault/changeset/ethbalmon_deploy.go | 16 +- ...balmon_set_keeper_registry_address_test.go | 155 +++++++++++ .../changeset/ethbalmon_set_watchlist.go | 15 +- .../changeset/ethbalmon_set_watchlist_test.go | 247 ++++++++++++++++++ .../ethbalmon_transfer_ownership_test.go | 144 ++++++++++ .../vault/changeset/ethbalmon_withdraw.go | 10 +- .../changeset/ethbalmon_withdraw_test.go | 201 ++++++++++++++ deployment/vault/changeset/types/types.go | 9 +- deployment/vault/changeset/validation.go | 7 +- 9 files changed, 773 insertions(+), 31 deletions(-) create mode 100644 deployment/vault/changeset/ethbalmon_set_keeper_registry_address_test.go create mode 100644 deployment/vault/changeset/ethbalmon_set_watchlist_test.go create mode 100644 deployment/vault/changeset/ethbalmon_transfer_ownership_test.go create mode 100644 deployment/vault/changeset/ethbalmon_withdraw_test.go diff --git a/deployment/vault/changeset/ethbalmon_deploy.go b/deployment/vault/changeset/ethbalmon_deploy.go index ff94eb2dda2..91361d5bf61 100644 --- a/deployment/vault/changeset/ethbalmon_deploy.go +++ b/deployment/vault/changeset/ethbalmon_deploy.go @@ -11,7 +11,6 @@ import ( "github.com/smartcontractkit/mcms" mcmstypes "github.com/smartcontractkit/mcms/types" - cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" ds "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -58,13 +57,20 @@ func (d deployEthBalMon) Apply(e cldf.Environment, config vaulttypes.DeployEthBa evmChains := e.BlockChains.EVMChains() - // Pick the first chain for deps (only needed for direct execution, not MCMS) - var primaryChain cldf_evm.Chain + // Pick a deterministic primary chain for deps (only needed for direct execution, not MCMS). + var ( + primaryChainSelector uint64 + primaryChainSet bool + ) for chainSelector := range config.Chains { - primaryChain = evmChains[chainSelector] - break + if !primaryChainSet || chainSelector < primaryChainSelector { + primaryChainSelector = chainSelector + primaryChainSet = true + } } + primaryChain := evmChains[primaryChainSelector] + deps := VaultDeps{ Auth: primaryChain.DeployerKey, Chain: primaryChain, diff --git a/deployment/vault/changeset/ethbalmon_set_keeper_registry_address_test.go b/deployment/vault/changeset/ethbalmon_set_keeper_registry_address_test.go new file mode 100644 index 00000000000..00e87d357f9 --- /dev/null +++ b/deployment/vault/changeset/ethbalmon_set_keeper_registry_address_test.go @@ -0,0 +1,155 @@ +package changeset + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-deployments-framework/operations/optest" + + "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" +) + +func TestValidateSetKeeperRegistryAddressConfig(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + selectorOther := chainselectors.TEST_90000002.Selector + + env, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, []uint64{selector}), + ) + require.NoError(t, err) + + tests := []struct { + name string + cfg types.EthBalMonSetKeeperRegistryAddressInput + wantError bool + errorMsg string + }{ + { + name: "empty chains", + cfg: types.EthBalMonSetKeeperRegistryAddressInput{Chains: map[uint64]types.SetKeeperRegistryChainConfig{}}, + wantError: true, + errorMsg: "no chains provided", + }, + { + name: "unknown chain selector", + cfg: types.EthBalMonSetKeeperRegistryAddressInput{ + Chains: map[uint64]types.SetKeeperRegistryChainConfig{ + math.MaxUint64: {NewKeeperRegistryAddress: testAddr1}, + }, + }, + wantError: true, + errorMsg: "not found in environment", + }, + { + name: "chain not in environment", + cfg: types.EthBalMonSetKeeperRegistryAddressInput{ + Chains: map[uint64]types.SetKeeperRegistryChainConfig{ + selectorOther: {NewKeeperRegistryAddress: testAddr1}, + }, + }, + wantError: true, + errorMsg: "not found in environment", + }, + { + name: "invalid keeper registry address", + cfg: types.EthBalMonSetKeeperRegistryAddressInput{ + Chains: map[uint64]types.SetKeeperRegistryChainConfig{ + selector: {NewKeeperRegistryAddress: "not-hex"}, + }, + }, + wantError: true, + errorMsg: "new_keeper_registry_address", + }, + { + name: "zero keeper registry address", + cfg: types.EthBalMonSetKeeperRegistryAddressInput{ + Chains: map[uint64]types.SetKeeperRegistryChainConfig{ + selector: {NewKeeperRegistryAddress: zeroAddr}, + }, + }, + wantError: true, + errorMsg: "cannot be zero address", + }, + { + name: "valid", + cfg: types.EthBalMonSetKeeperRegistryAddressInput{ + Chains: map[uint64]types.SetKeeperRegistryChainConfig{ + selector: {NewKeeperRegistryAddress: testAddr1}, + }, + }, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := ValidateSetKeeperRegistryAddressConfig(t.Context(), *env, tt.cfg) + if tt.wantError { + require.Error(t, err) + if tt.errorMsg != "" { + require.Contains(t, err.Error(), tt.errorMsg) + } + } else { + require.NoError(t, err) + } + }) + } +} + +func TestSetKeeperRegistrySequence_noChains(t *testing.T) { + t.Parallel() + + b := optest.NewBundle(t) + _, err := operations.ExecuteSequence(b, SetKeeperRegistrySequence, VaultDeps{}, EthBalMonSetKeeperRegistryAddressSequenceInput{}) + require.Error(t, err) + require.Contains(t, err.Error(), "no chains provided") +} + +func TestSetKeeperRegistryAddressChangeset(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + deployCfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: {SetKeeperRegistryAddress: testAddr1}, + }, + } + deployTask := runtime.ChangesetTask(DeployEthBalMonChangeSet, deployCfg) + require.NoError(t, rt.Exec(deployTask)) + + cfg := types.EthBalMonSetKeeperRegistryAddressInput{ + Chains: map[uint64]types.SetKeeperRegistryChainConfig{ + selector: {NewKeeperRegistryAddress: testAddr2}, + }, + } + require.NoError(t, SetKeeperRegistryAddress.VerifyPreconditions(rt.Environment(), cfg)) + + keeperTask := runtime.ChangesetTask(SetKeeperRegistryAddress, cfg) + require.NoError(t, rt.Exec(keeperTask)) + + out := rt.State().Outputs[keeperTask.ID()] + require.NotEmpty(t, out.MCMSTimelockProposals) + prop := out.MCMSTimelockProposals[0] + require.Contains(t, prop.Description, "EthBalMon SetKeeperRegistryAddress") + require.Len(t, prop.Operations, 1) + require.Len(t, prop.Operations[0].Transactions, 1) + require.Contains(t, prop.Operations[0].Transactions[0].Tags, "setKeeperRegistryAddress") +} diff --git a/deployment/vault/changeset/ethbalmon_set_watchlist.go b/deployment/vault/changeset/ethbalmon_set_watchlist.go index 7415aa4503e..4528485ea54 100644 --- a/deployment/vault/changeset/ethbalmon_set_watchlist.go +++ b/deployment/vault/changeset/ethbalmon_set_watchlist.go @@ -117,8 +117,8 @@ var EthBalMonSetWatchListSequence = operations.NewSequence( type EthBalMonSetWatchListOpInput struct { ChainSelector uint64 `json:"chain_selector"` Addresses []common.Address `json:"addresses"` - MinBalancesWei []big.Int `json:"min_balance_wei"` - TopUpAmountsWei []big.Int `json:"topup_amounts_wei"` + MinBalancesWei []*big.Int `json:"min_balance_wei"` + TopUpAmountsWei []*big.Int `json:"topup_amounts_wei"` MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } @@ -182,16 +182,7 @@ var EthBalMonSetWatchListOperation = operations.NewOperation( fmt.Errorf("failed to instantiate EthBalanceMonitor at %s: %w", ethBalMonAddr, err) } - minBalancesWei := make([]*big.Int, len(input.MinBalancesWei)) - for i := range input.MinBalancesWei { - minBalancesWei[i] = &input.MinBalancesWei[i] - } - - topAmountsWei := make([]*big.Int, len(input.TopUpAmountsWei)) - for i := range input.TopUpAmountsWei { - topAmountsWei[i] = &input.TopUpAmountsWei[i] - } - setWatchListTx, err := ethBalMon.SetWatchList(cldf.SimTransactOpts(), input.Addresses, minBalancesWei, topAmountsWei) + setWatchListTx, err := ethBalMon.SetWatchList(cldf.SimTransactOpts(), input.Addresses, input.MinBalancesWei, input.TopUpAmountsWei) if err != nil { return EthBalMonSetWatchListOpOutput{}, fmt.Errorf("failed to generate setWatchList calldata on chain %d: %w ", input.ChainSelector, err) } diff --git a/deployment/vault/changeset/ethbalmon_set_watchlist_test.go b/deployment/vault/changeset/ethbalmon_set_watchlist_test.go new file mode 100644 index 00000000000..7e4b932aad3 --- /dev/null +++ b/deployment/vault/changeset/ethbalmon_set_watchlist_test.go @@ -0,0 +1,247 @@ +package changeset + +import ( + "math" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" + + "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" +) + +func TestValidateEthBalMonSetWatchListConfig(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + selectorOther := chainselectors.TEST_90000002.Selector + + env, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, []uint64{selector}), + ) + require.NoError(t, err) + + addr := common.HexToAddress(testAddr1) + minOne := big.NewInt(1) + topOne := big.NewInt(1) + neg := big.NewInt(-1) + + tests := []struct { + name string + cfg types.EthBalMonSetWatchListInput + wantError bool + errorMsg string + }{ + { + name: "empty chains", + cfg: types.EthBalMonSetWatchListInput{Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{}}, + wantError: true, + errorMsg: "no chains provided", + }, + { + name: "unknown chain selector", + cfg: types.EthBalMonSetWatchListInput{ + Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{ + math.MaxUint64: { + Addresses: []common.Address{addr}, + MinBalancesWei: []*big.Int{minOne}, + TopUpAmountsWei: []*big.Int{topOne}, + }, + }, + }, + wantError: true, + errorMsg: "not found in environment", + }, + { + name: "chain not in environment", + cfg: types.EthBalMonSetWatchListInput{ + Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{ + selectorOther: { + Addresses: []common.Address{addr}, + MinBalancesWei: []*big.Int{minOne}, + TopUpAmountsWei: []*big.Int{topOne}, + }, + }, + }, + wantError: true, + errorMsg: "not found in environment", + }, + { + name: "empty addresses", + cfg: types.EthBalMonSetWatchListInput{ + Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{ + selector: { + Addresses: nil, + MinBalancesWei: nil, + TopUpAmountsWei: nil, + }, + }, + }, + wantError: true, + errorMsg: "addresses must not be empty", + }, + { + name: "slice length mismatch", + cfg: types.EthBalMonSetWatchListInput{ + Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{ + selector: { + Addresses: []common.Address{addr, addr}, + MinBalancesWei: []*big.Int{minOne}, + TopUpAmountsWei: []*big.Int{topOne, topOne}, + }, + }, + }, + wantError: true, + errorMsg: "must have the same length", + }, + { + name: "negative min balance", + cfg: types.EthBalMonSetWatchListInput{ + Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{ + selector: { + Addresses: []common.Address{addr}, + MinBalancesWei: []*big.Int{neg}, + TopUpAmountsWei: []*big.Int{topOne}, + }, + }, + }, + wantError: true, + errorMsg: "min_balance_wei at index 0 must be >= 0", + }, + { + name: "negative top-up", + cfg: types.EthBalMonSetWatchListInput{ + Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{ + selector: { + Addresses: []common.Address{addr}, + MinBalancesWei: []*big.Int{minOne}, + TopUpAmountsWei: []*big.Int{neg}, + }, + }, + }, + wantError: true, + errorMsg: "topup_amounts_wei at index 0 must be >= 0", + }, + { + name: "valid", + cfg: types.EthBalMonSetWatchListInput{ + Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{ + selector: { + Addresses: []common.Address{addr}, + MinBalancesWei: []*big.Int{minOne}, + TopUpAmountsWei: []*big.Int{topOne}, + }, + }, + }, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := ValidateEthBalMonSetWatchListConfig(t.Context(), *env, tt.cfg) + if tt.wantError { + require.Error(t, err) + if tt.errorMsg != "" { + require.Contains(t, err.Error(), tt.errorMsg) + } + } else { + require.NoError(t, err) + } + }) + } +} + +func TestEthBalMonSetWatchListChangeset(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + deployCfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: {SetKeeperRegistryAddress: testAddr1}, + }, + } + deployTask := runtime.ChangesetTask(DeployEthBalMonChangeSet, deployCfg) + require.NoError(t, rt.Exec(deployTask)) + + watchAddr := common.HexToAddress(testAddr2) + cfg := types.EthBalMonSetWatchListInput{ + Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{ + selector: { + Addresses: []common.Address{watchAddr}, + MinBalancesWei: []*big.Int{big.NewInt(1)}, + TopUpAmountsWei: []*big.Int{big.NewInt(2)}, + }, + }, + } + require.NoError(t, EthBalMonSetWatchList.VerifyPreconditions(rt.Environment(), cfg)) + + watchTask := runtime.ChangesetTask(EthBalMonSetWatchList, cfg) + require.NoError(t, rt.Exec(watchTask)) + + out := rt.State().Outputs[watchTask.ID()] + require.NotEmpty(t, out.MCMSTimelockProposals) + prop := out.MCMSTimelockProposals[0] + require.Contains(t, prop.Description, "EthBalMon SetWatchList") + require.Len(t, prop.Operations, 1) + require.Len(t, prop.Operations[0].Transactions, 1) + require.Contains(t, prop.Operations[0].Transactions[0].Tags, "setWatchList") +} + +func TestEthBalMonSetWatchList_VerifyPreconditions_rejectsEmptyChains(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + err = EthBalMonSetWatchList.VerifyPreconditions(rt.Environment(), types.EthBalMonSetWatchListInput{ + Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{}, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "no chains provided") +} + +func TestEthBalMonSetWatchList_Apply_withoutEthBalMonInDatastore(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + cfg := types.EthBalMonSetWatchListInput{ + Chains: map[uint64]types.EthBalMonSetWatchListChainConfig{ + selector: { + Addresses: []common.Address{common.HexToAddress(testAddr1)}, + MinBalancesWei: []*big.Int{big.NewInt(1)}, + TopUpAmountsWei: []*big.Int{big.NewInt(1)}, + }, + }, + } + require.NoError(t, EthBalMonSetWatchList.VerifyPreconditions(rt.Environment(), cfg)) + + _, err = EthBalMonSetWatchList.Apply(rt.Environment(), cfg) + require.Error(t, err) +} diff --git a/deployment/vault/changeset/ethbalmon_transfer_ownership_test.go b/deployment/vault/changeset/ethbalmon_transfer_ownership_test.go new file mode 100644 index 00000000000..3f8a1952071 --- /dev/null +++ b/deployment/vault/changeset/ethbalmon_transfer_ownership_test.go @@ -0,0 +1,144 @@ +package changeset + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" + + "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" +) + +func TestValidateEthBalMonTransferOwnershipConfig(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + selectorOther := chainselectors.TEST_90000002.Selector + + env, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, []uint64{selector}), + ) + require.NoError(t, err) + + tests := []struct { + name string + cfg types.EthBalMonTransferOwnershipInput + wantError bool + errorMsg string + }{ + { + name: "empty chains", + cfg: types.EthBalMonTransferOwnershipInput{Chains: map[uint64]types.EthBalMonTransferOwnershipChainConfig{}}, + wantError: true, + errorMsg: "no chains provided", + }, + { + name: "unknown chain selector", + cfg: types.EthBalMonTransferOwnershipInput{ + Chains: map[uint64]types.EthBalMonTransferOwnershipChainConfig{ + math.MaxUint64: {NewOwner: testAddr1}, + }, + }, + wantError: true, + errorMsg: "not found in environment", + }, + { + name: "chain not in environment", + cfg: types.EthBalMonTransferOwnershipInput{ + Chains: map[uint64]types.EthBalMonTransferOwnershipChainConfig{ + selectorOther: {NewOwner: testAddr1}, + }, + }, + wantError: true, + errorMsg: "not found in environment", + }, + { + name: "invalid new owner", + cfg: types.EthBalMonTransferOwnershipInput{ + Chains: map[uint64]types.EthBalMonTransferOwnershipChainConfig{ + selector: {NewOwner: "not-hex"}, + }, + }, + wantError: true, + errorMsg: "newOwner", + }, + { + name: "zero new owner", + cfg: types.EthBalMonTransferOwnershipInput{ + Chains: map[uint64]types.EthBalMonTransferOwnershipChainConfig{ + selector: {NewOwner: zeroAddr}, + }, + }, + wantError: true, + errorMsg: "cannot be zero address", + }, + { + name: "valid", + cfg: types.EthBalMonTransferOwnershipInput{ + Chains: map[uint64]types.EthBalMonTransferOwnershipChainConfig{ + selector: {NewOwner: testAddr1}, + }, + }, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := ValidateEthBalMonTransferOwnershipConfig(t.Context(), *env, tt.cfg) + if tt.wantError { + require.Error(t, err) + if tt.errorMsg != "" { + require.Contains(t, err.Error(), tt.errorMsg) + } + } else { + require.NoError(t, err) + } + }) + } +} + +func TestEthBalMonTransferOwnershipChangeset(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + deployCfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: {SetKeeperRegistryAddress: testAddr1}, + }, + } + deployTask := runtime.ChangesetTask(DeployEthBalMonChangeSet, deployCfg) + require.NoError(t, rt.Exec(deployTask)) + + cfg := types.EthBalMonTransferOwnershipInput{ + Chains: map[uint64]types.EthBalMonTransferOwnershipChainConfig{ + selector: {NewOwner: testAddr2}, + }, + } + require.NoError(t, EthBalMonTransferOwnership.VerifyPreconditions(rt.Environment(), cfg)) + + transferTask := runtime.ChangesetTask(EthBalMonTransferOwnership, cfg) + require.NoError(t, rt.Exec(transferTask)) + + out := rt.State().Outputs[transferTask.ID()] + require.NotEmpty(t, out.MCMSTimelockProposals) + prop := out.MCMSTimelockProposals[0] + require.Contains(t, prop.Description, "EthBalMon transferOwnership") + require.Len(t, prop.Operations, 1) + require.Len(t, prop.Operations[0].Transactions, 1) + require.Contains(t, prop.Operations[0].Transactions[0].Tags, "transferOwnership") +} diff --git a/deployment/vault/changeset/ethbalmon_withdraw.go b/deployment/vault/changeset/ethbalmon_withdraw.go index 5edd89532f9..788fa025740 100644 --- a/deployment/vault/changeset/ethbalmon_withdraw.go +++ b/deployment/vault/changeset/ethbalmon_withdraw.go @@ -83,7 +83,7 @@ var EthBalMonWithdrawSequence = operations.NewSequence( opReport, err := operations.ExecuteOperation(b, EthBalMonWithdrawOperation, deps, EthBalMonWithdrawOpInput{ ChainSelector: chainSelector, Amount: chainConfig.Amount, - Payeer: chainConfig.Payeer, + Payee: chainConfig.Payee, MCMSConfig: input.MCMSConfig, }) if err != nil { @@ -113,7 +113,7 @@ var EthBalMonWithdrawSequence = operations.NewSequence( type EthBalMonWithdrawOpInput struct { ChainSelector uint64 `json:"chain_selector"` Amount *big.Int `json:"amount"` - Payeer string `json:"payeer"` + Payee string `json:"payee"` MCMSConfig *proposalutils.TimelockConfig `json:"mcms_config,omitempty"` } @@ -174,9 +174,9 @@ var EthBalMonWithdrawOperation = operations.NewOperation( fmt.Errorf("failed to instantiate EthBalanceMonitor at %s: %w", ethBalMonAddr, err) } - withdrawTx, err := ethBalMon.Withdraw(cldf.SimTransactOpts(), input.Amount, common.HexToAddress(input.Payeer)) + withdrawTx, err := ethBalMon.Withdraw(cldf.SimTransactOpts(), input.Amount, common.HexToAddress(input.Payee)) if err != nil { - return EthBalMonWithdrawOpOutput{}, fmt.Errorf("failed to generate withdraw calldata on chain %d: %w ", input.ChainSelector, err) + return EthBalMonWithdrawOpOutput{}, fmt.Errorf("failed to generate withdraw calldata on chain %d: %w", input.ChainSelector, err) } batch := mcmstypes.BatchOperation{ ChainSelector: mcmstypes.ChainSelector(input.ChainSelector), @@ -199,7 +199,7 @@ var EthBalMonWithdrawOperation = operations.NewOperation( "chainSelector", input.ChainSelector, "ethBalMon", ethBalMonAddr, "amount", input.Amount, - "payeer", input.Payeer, + "payee", input.Payee, ) return EthBalMonWithdrawOpOutput{ diff --git a/deployment/vault/changeset/ethbalmon_withdraw_test.go b/deployment/vault/changeset/ethbalmon_withdraw_test.go new file mode 100644 index 00000000000..574c1255df3 --- /dev/null +++ b/deployment/vault/changeset/ethbalmon_withdraw_test.go @@ -0,0 +1,201 @@ +package changeset + +import ( + "math" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" + + "github.com/smartcontractkit/chainlink/deployment/vault/changeset/types" +) + +func TestValidateEthBalMonWithdrawConfig(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + selectorOther := chainselectors.TEST_90000002.Selector + + env, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, []uint64{selector}), + ) + require.NoError(t, err) + + tests := []struct { + name string + cfg types.EthBalMonWithdrawInput + wantError bool + errorMsg string + }{ + { + name: "empty chains", + cfg: types.EthBalMonWithdrawInput{Chains: map[uint64]types.EthBalMonWithdrawChainConfig{}}, + wantError: true, + errorMsg: "no chains provided", + }, + { + name: "unknown chain selector", + cfg: types.EthBalMonWithdrawInput{ + Chains: map[uint64]types.EthBalMonWithdrawChainConfig{ + math.MaxUint64: {Amount: big.NewInt(1), Payee: testAddr1}, + }, + }, + wantError: true, + errorMsg: "not found in environment", + }, + { + name: "chain not in environment", + cfg: types.EthBalMonWithdrawInput{ + Chains: map[uint64]types.EthBalMonWithdrawChainConfig{ + selectorOther: {Amount: big.NewInt(1), Payee: testAddr1}, + }, + }, + wantError: true, + errorMsg: "not found in environment", + }, + { + name: "nil amount", + cfg: types.EthBalMonWithdrawInput{ + Chains: map[uint64]types.EthBalMonWithdrawChainConfig{ + selector: {Amount: nil, Payee: testAddr1}, + }, + }, + wantError: true, + errorMsg: "amount to withdraw must be positive", + }, + { + name: "zero amount", + cfg: types.EthBalMonWithdrawInput{ + Chains: map[uint64]types.EthBalMonWithdrawChainConfig{ + selector: {Amount: big.NewInt(0), Payee: testAddr1}, + }, + }, + wantError: true, + errorMsg: "amount to withdraw must be positive", + }, + { + name: "negative amount", + cfg: types.EthBalMonWithdrawInput{ + Chains: map[uint64]types.EthBalMonWithdrawChainConfig{ + selector: {Amount: big.NewInt(-1), Payee: testAddr1}, + }, + }, + wantError: true, + errorMsg: "amount to withdraw must be positive", + }, + { + name: "invalid payee", + cfg: types.EthBalMonWithdrawInput{ + Chains: map[uint64]types.EthBalMonWithdrawChainConfig{ + selector: {Amount: big.NewInt(1), Payee: "not-hex"}, + }, + }, + wantError: true, + errorMsg: "payee", + }, + { + name: "zero payee", + cfg: types.EthBalMonWithdrawInput{ + Chains: map[uint64]types.EthBalMonWithdrawChainConfig{ + selector: {Amount: big.NewInt(1), Payee: zeroAddr}, + }, + }, + wantError: true, + errorMsg: "payeer address cannot be zero address", + }, + { + name: "valid", + cfg: types.EthBalMonWithdrawInput{ + Chains: map[uint64]types.EthBalMonWithdrawChainConfig{ + selector: {Amount: big.NewInt(1), Payee: testAddr1}, + }, + }, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := ValidateEthBalMonWithdrawConfig(t.Context(), *env, tt.cfg) + if tt.wantError { + require.Error(t, err) + if tt.errorMsg != "" { + require.Contains(t, err.Error(), tt.errorMsg) + } + } else { + require.NoError(t, err) + } + }) + } +} + +func TestEthBalMonWithdrawChangeset(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + deployCfg := types.DeployEthBalMonInput{ + Chains: map[uint64]types.DeployEthBalMonChainConfig{ + selector: {SetKeeperRegistryAddress: testAddr1}, + }, + } + deployTask := runtime.ChangesetTask(DeployEthBalMonChangeSet, deployCfg) + require.NoError(t, rt.Exec(deployTask)) + + cfg := types.EthBalMonWithdrawInput{ + Chains: map[uint64]types.EthBalMonWithdrawChainConfig{ + selector: { + Amount: big.NewInt(1), + Payee: testAddr2, + }, + }, + } + require.NoError(t, EthBalMonWithdraw.VerifyPreconditions(rt.Environment(), cfg)) + + withdrawTask := runtime.ChangesetTask(EthBalMonWithdraw, cfg) + require.NoError(t, rt.Exec(withdrawTask)) + + out := rt.State().Outputs[withdrawTask.ID()] + require.NotEmpty(t, out.MCMSTimelockProposals) + prop := out.MCMSTimelockProposals[0] + require.Contains(t, prop.Description, "EthBalMon Withdraw") + require.Len(t, prop.Operations, 1) + require.Len(t, prop.Operations[0].Transactions, 1) + require.Contains(t, prop.Operations[0].Transactions[0].Tags, "withdraw") +} + +func TestEthBalMonWithdraw_Apply_withoutEthBalMonInDatastore(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + setupMCMSInfrastructure(t, rt, []uint64{selector}) + fundDeployerAccounts(t, rt.Environment(), []uint64{selector}) + + cfg := types.EthBalMonWithdrawInput{ + Chains: map[uint64]types.EthBalMonWithdrawChainConfig{ + selector: {Amount: big.NewInt(1), Payee: testAddr1}, + }, + } + require.NoError(t, EthBalMonWithdraw.VerifyPreconditions(rt.Environment(), cfg)) + + _, err = EthBalMonWithdraw.Apply(rt.Environment(), cfg) + require.Error(t, err) +} diff --git a/deployment/vault/changeset/types/types.go b/deployment/vault/changeset/types/types.go index 4c841bd8e96..c849587bc8f 100644 --- a/deployment/vault/changeset/types/types.go +++ b/deployment/vault/changeset/types/types.go @@ -113,10 +113,11 @@ type EthBalMonSetKeeperRegistryAddressInput struct { // EthBalMonSetWatchListChainConfig replaces the monitored addresses and thresholds on one chain. // Addresses, MinBalancesWei, and TopUpAmountsWei are parallel slices: index i applies to Addresses[i]. +// MinBalancesWei and TopUpAmountsWei are represented as *big.Int values in wei. type EthBalMonSetWatchListChainConfig struct { Addresses []common.Address `json:"addresses"` - MinBalancesWei []big.Int `json:"min_balance_wei"` - TopUpAmountsWei []big.Int `json:"topup_amounts_wei"` + MinBalancesWei []*big.Int `json:"min_balance_wei"` + TopUpAmountsWei []*big.Int `json:"topup_amounts_wei"` } // EthBalMonSetWatchListInput is the input to the setWatchList changeset. @@ -131,8 +132,8 @@ type EthBalMonSetWatchListInput struct { type EthBalMonWithdrawChainConfig struct { // Amount is the withdrawal amount in wei. Must be positive (validated by the changeset). Amount *big.Int `json:"amount"` - // Payeer is the recipient address (hex). JSON key is "payeer" for backward compatibility. - Payeer string `json:"payeer"` + // Payee is the recipient address (hex). + Payee string `json:"payee"` } // EthBalMonWithdrawInput is the input to the EthBalMon withdraw changeset. diff --git a/deployment/vault/changeset/validation.go b/deployment/vault/changeset/validation.go index 011d7cc8e78..78f96b6c917 100644 --- a/deployment/vault/changeset/validation.go +++ b/deployment/vault/changeset/validation.go @@ -245,9 +245,6 @@ func ValidateDeployEthBalMonConfig(ctx context.Context, env cldf.Environment, cf if err := validateChainSelector(chainSelector, env); err != nil { return fmt.Errorf("chain %d: %w", chainSelector, err) } - if chainCfg.SetKeeperRegistryAddress == "" { - return fmt.Errorf("chain %d: setKeeperRegistryAddress must not be empty", chainSelector) - } if err := validateEthAddress("setKeeperRegistryAddress", chainCfg.SetKeeperRegistryAddress); err != nil { return fmt.Errorf("chain %d: %w", chainSelector, err) } @@ -322,10 +319,10 @@ func ValidateEthBalMonWithdrawConfig(ctx context.Context, env cldf.Environment, return fmt.Errorf("chain %d: amount to withdraw must be positive", chainSelector) } - if err := validateEthAddress("payeer", chainConfig.Payeer); err != nil { + if err := validateEthAddress("payee", chainConfig.Payee); err != nil { return fmt.Errorf("chain %d: %w", chainSelector, err) } - if common.HexToAddress(chainConfig.Payeer) == (common.Address{}) { + if common.HexToAddress(chainConfig.Payee) == (common.Address{}) { return fmt.Errorf("chain %d: payeer address cannot be zero address", chainSelector) } }