From 3e5ec2334f446b9b12cfce6da663b839cba14491 Mon Sep 17 00:00:00 2001 From: karen-stepanyan <91897037+karen-stepanyan@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:02:59 +0400 Subject: [PATCH] Added data feeds deployment changesets (#16323) * add data feeds deployment changesets * fix lint issues pt1 * fix lint issues pt2 * fix major lint issues * fix goimports * replace if/else with switch in state * replace mcms with mcmsv2 * create BuildMCMProposal func * add acceptOwnership changeset * lint * add tests for mcms, accept_ownership changeset * fix lint issues * use changesetv2 * add test cases for mcms * fix lint issues * fix cache_deploy test * add mcms support to confirm/propose aggregator changesets. buildproposal batch * fix lint issues * remove custom wrapper for legacy changeset * add more complex changesets * fix lint * minor changes * fix lint * fix typo * update buildproposal env --- .../data-feeds/changeset/accept_ownership.go | 56 ++++++ .../changeset/accept_ownership_test.go | 65 +++++++ .../changeset/confirm_aggregator.go | 69 ++++++++ .../changeset/confirm_aggregator_test.go | 118 +++++++++++++ deployment/data-feeds/changeset/deploy.go | 78 +++++++++ .../changeset/deploy_aggregator_proxy.go | 70 ++++++++ .../changeset/deploy_aggregator_proxy_test.go | 52 ++++++ .../data-feeds/changeset/deploy_cache.go | 44 +++++ .../data-feeds/changeset/deploy_cache_test.go | 43 +++++ .../changeset/import_to_addressbook.go | 58 +++++++ .../changeset/import_to_addressbook_test.go | 48 ++++++ .../data-feeds/changeset/migrate_feeds.go | 101 +++++++++++ .../changeset/migrate_feeds_test.go | 79 +++++++++ .../changeset/new_feed_with_proxy.go | 144 ++++++++++++++++ .../changeset/new_feed_with_proxy_test.go | 111 ++++++++++++ deployment/data-feeds/changeset/proposal.go | 64 +++++++ .../changeset/propose_aggregator.go | 68 ++++++++ .../changeset/propose_aggregator_test.go | 101 +++++++++++ .../changeset/remove_dataid_proxy_mapping.go | 69 ++++++++ .../remove_dataid_proxy_mapping_test.go | 144 ++++++++++++++++ .../data-feeds/changeset/remove_feed.go | 92 ++++++++++ .../changeset/remove_feed_config.go | 64 +++++++ .../changeset/remove_feed_config_test.go | 160 +++++++++++++++++ .../data-feeds/changeset/remove_feed_test.go | 162 ++++++++++++++++++ .../data-feeds/changeset/set_feed_admin.go | 65 +++++++ .../changeset/set_feed_admin_test.go | 98 +++++++++++ .../data-feeds/changeset/set_feed_config.go | 73 ++++++++ .../changeset/set_feed_config_test.go | 138 +++++++++++++++ deployment/data-feeds/changeset/state.go | 143 ++++++++++++++++ .../changeset/testdata/import_addresses.json | 18 ++ .../changeset/testdata/migrate_feeds.json | 20 +++ .../data-feeds/changeset/types/types.go | 133 ++++++++++++++ .../changeset/update_data_id_proxy.go | 73 ++++++++ .../changeset/update_data_id_proxy_test.go | 122 +++++++++++++ deployment/data-feeds/changeset/validation.go | 45 +++++ deployment/data-feeds/changeset/view.go | 26 +++ deployment/data-feeds/shared/utils.go | 50 ++++++ .../data-feeds/view/v1_0/cache_contract.go | 28 +++ .../data-feeds/view/v1_0/proxy_contract.go | 48 ++++++ deployment/data-feeds/view/view.go | 31 ++++ 40 files changed, 3171 insertions(+) create mode 100644 deployment/data-feeds/changeset/accept_ownership.go create mode 100644 deployment/data-feeds/changeset/accept_ownership_test.go create mode 100644 deployment/data-feeds/changeset/confirm_aggregator.go create mode 100644 deployment/data-feeds/changeset/confirm_aggregator_test.go create mode 100644 deployment/data-feeds/changeset/deploy.go create mode 100644 deployment/data-feeds/changeset/deploy_aggregator_proxy.go create mode 100644 deployment/data-feeds/changeset/deploy_aggregator_proxy_test.go create mode 100644 deployment/data-feeds/changeset/deploy_cache.go create mode 100644 deployment/data-feeds/changeset/deploy_cache_test.go create mode 100644 deployment/data-feeds/changeset/import_to_addressbook.go create mode 100644 deployment/data-feeds/changeset/import_to_addressbook_test.go create mode 100644 deployment/data-feeds/changeset/migrate_feeds.go create mode 100644 deployment/data-feeds/changeset/migrate_feeds_test.go create mode 100644 deployment/data-feeds/changeset/new_feed_with_proxy.go create mode 100644 deployment/data-feeds/changeset/new_feed_with_proxy_test.go create mode 100644 deployment/data-feeds/changeset/proposal.go create mode 100644 deployment/data-feeds/changeset/propose_aggregator.go create mode 100644 deployment/data-feeds/changeset/propose_aggregator_test.go create mode 100644 deployment/data-feeds/changeset/remove_dataid_proxy_mapping.go create mode 100644 deployment/data-feeds/changeset/remove_dataid_proxy_mapping_test.go create mode 100644 deployment/data-feeds/changeset/remove_feed.go create mode 100644 deployment/data-feeds/changeset/remove_feed_config.go create mode 100644 deployment/data-feeds/changeset/remove_feed_config_test.go create mode 100644 deployment/data-feeds/changeset/remove_feed_test.go create mode 100644 deployment/data-feeds/changeset/set_feed_admin.go create mode 100644 deployment/data-feeds/changeset/set_feed_admin_test.go create mode 100644 deployment/data-feeds/changeset/set_feed_config.go create mode 100644 deployment/data-feeds/changeset/set_feed_config_test.go create mode 100644 deployment/data-feeds/changeset/state.go create mode 100644 deployment/data-feeds/changeset/testdata/import_addresses.json create mode 100644 deployment/data-feeds/changeset/testdata/migrate_feeds.json create mode 100644 deployment/data-feeds/changeset/types/types.go create mode 100644 deployment/data-feeds/changeset/update_data_id_proxy.go create mode 100644 deployment/data-feeds/changeset/update_data_id_proxy_test.go create mode 100644 deployment/data-feeds/changeset/validation.go create mode 100644 deployment/data-feeds/changeset/view.go create mode 100644 deployment/data-feeds/shared/utils.go create mode 100644 deployment/data-feeds/view/v1_0/cache_contract.go create mode 100644 deployment/data-feeds/view/v1_0/proxy_contract.go create mode 100644 deployment/data-feeds/view/view.go diff --git a/deployment/data-feeds/changeset/accept_ownership.go b/deployment/data-feeds/changeset/accept_ownership.go new file mode 100644 index 00000000000..726d782404a --- /dev/null +++ b/deployment/data-feeds/changeset/accept_ownership.go @@ -0,0 +1,56 @@ +package changeset + +import ( + "errors" + "fmt" + + mcmslib "github.com/smartcontractkit/mcms" + + "github.com/smartcontractkit/chainlink/deployment" + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" +) + +// AcceptOwnershipChangeset is a changeset that will create an MCM proposal to accept the ownership of a contract. +// Returns an MSM proposal to accept the ownership of a contract. Doesn't return a new addressbook. +// Once proposal is executed, new owned contract can be imported into the addressbook. +var AcceptOwnershipChangeset = deployment.CreateChangeSet(acceptOwnershipLogic, acceptOwnershipPrecondition) + +func acceptOwnershipLogic(env deployment.Environment, c types.AcceptOwnershipConfig) (deployment.ChangesetOutput, error) { + chain := env.Chains[c.ChainSelector] + + _, contract, err := commonChangesets.LoadOwnableContract(c.ContractAddress, chain.Client) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load the contract %w", err) + } + + tx, err := contract.AcceptOwnership(deployment.SimTransactOpts()) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create accept transfer ownership tx %w", err) + } + + proposal, err := BuildMCMProposals(env, "accept ownership to timelock", c.ChainSelector, []ProposalData{ + { + contract: c.ContractAddress.Hex(), + tx: tx, + }, + }, c.McmsConfig.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + + return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil +} + +func acceptOwnershipPrecondition(env deployment.Environment, c types.AcceptOwnershipConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + if c.McmsConfig == nil { + return errors.New("mcms config is required") + } + + return ValidateMCMSAddresses(env.ExistingAddresses, c.ChainSelector) +} diff --git a/deployment/data-feeds/changeset/accept_ownership_test.go b/deployment/data-feeds/changeset/accept_ownership_test.go new file mode 100644 index 00000000000..60ac25d9d24 --- /dev/null +++ b/deployment/data-feeds/changeset/accept_ownership_test.go @@ -0,0 +1,65 @@ +package changeset + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + + "github.com/smartcontractkit/chainlink/deployment" + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestAcceptOwnership(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + chain := env.Chains[chainSelector] + + newEnv, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.DeployMCMSWithTimelockV2), + map[uint64]commonTypes.MCMSWithTimelockConfigV2{ + chainSelector: proposalutils.SingleGroupTimelockConfigV2(t), + }, + ), + ) + require.NoError(t, err) + + timeLockAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "RBACTimelock") + require.NoError(t, err) + + cache, _ := DeployCache(chain, []string{}) + tx, _ := cache.Contract.TransferOwnership(chain.DeployerKey, common.HexToAddress(timeLockAddress)) + _, err = chain.Confirm(tx) + require.NoError(t, err) + + _, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + AcceptOwnershipChangeset, + types.AcceptOwnershipConfig{ + ChainSelector: chainSelector, + ContractAddress: cache.Contract.Address(), + McmsConfig: &types.MCMSConfig{ + MinDelay: 1, + }, + }, + ), + ) + require.NoError(t, err) +} diff --git a/deployment/data-feeds/changeset/confirm_aggregator.go b/deployment/data-feeds/changeset/confirm_aggregator.go new file mode 100644 index 00000000000..e4cc4cb919a --- /dev/null +++ b/deployment/data-feeds/changeset/confirm_aggregator.go @@ -0,0 +1,69 @@ +package changeset + +import ( + "fmt" + + mcmslib "github.com/smartcontractkit/mcms" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + proxy "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/aggregator_proxy" +) + +// ConfirmAggregatorChangeset is a changeset that confirms a proposed aggregator on deployed AggregatorProxy contract +// This changeset may return a timelock proposal if the MCMS config is provided, otherwise it will execute the transaction with the deployer key. +var ConfirmAggregatorChangeset = deployment.CreateChangeSet(confirmAggregatorLogic, confirmAggregatorPrecondition) + +func confirmAggregatorLogic(env deployment.Environment, c types.ProposeConfirmAggregatorConfig) (deployment.ChangesetOutput, error) { + chain := env.Chains[c.ChainSelector] + + aggregatorProxy, err := proxy.NewAggregatorProxy(c.ProxyAddress, chain.Client) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load AggregatorProxy: %w", err) + } + + txOpt := chain.DeployerKey + if c.McmsConfig != nil { + txOpt = deployment.SimTransactOpts() + } + + tx, err := aggregatorProxy.ConfirmAggregator(txOpt, c.NewAggregatorAddress) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to execute ConfirmAggregator: %w", err) + } + + if c.McmsConfig != nil { + proposal, err := BuildMCMProposals(env, "proposal to confirm a new aggregator", c.ChainSelector, []ProposalData{ + { + contract: aggregatorProxy.Address().Hex(), + tx: tx, + }, + }, c.McmsConfig.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil + } + + _, err = chain.Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", tx.Hash().String(), err) + } + + return deployment.ChangesetOutput{}, nil +} + +func confirmAggregatorPrecondition(env deployment.Environment, c types.ProposeConfirmAggregatorConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + if c.McmsConfig != nil { + if err := ValidateMCMSAddresses(env.ExistingAddresses, c.ChainSelector); err != nil { + return err + } + } + + return nil +} diff --git a/deployment/data-feeds/changeset/confirm_aggregator_test.go b/deployment/data-feeds/changeset/confirm_aggregator_test.go new file mode 100644 index 00000000000..2dcace3dbf3 --- /dev/null +++ b/deployment/data-feeds/changeset/confirm_aggregator_test.go @@ -0,0 +1,118 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestConfirmAggregator(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + // without MCMS + newEnv, err := commonChangesets.Apply(t, env, nil, + // Deploy cache and aggregator proxy + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + commonChangesets.Configure( + changeset.DeployAggregatorProxyChangeset, + types.DeployAggregatorProxyConfig{ + ChainsToDeploy: []uint64{chainSelector}, + AccessController: []common.Address{common.HexToAddress("0x")}, + }, + ), + ) + require.NoError(t, err) + + proxyAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "AggregatorProxy") + require.NoError(t, err) + + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // Propose and confirm new Aggregator + commonChangesets.Configure( + changeset.ProposeAggregatorChangeset, + types.ProposeConfirmAggregatorConfig{ + ChainSelector: chainSelector, + ProxyAddress: common.HexToAddress(proxyAddress), + NewAggregatorAddress: common.HexToAddress("0x123"), + }, + ), + commonChangesets.Configure( + changeset.ConfirmAggregatorChangeset, + types.ProposeConfirmAggregatorConfig{ + ChainSelector: chainSelector, + ProxyAddress: common.HexToAddress(proxyAddress), + NewAggregatorAddress: common.HexToAddress("0x123"), + }, + ), + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.DeployMCMSWithTimelockV2), + map[uint64]commonTypes.MCMSWithTimelockConfigV2{ + chainSelector: proposalutils.SingleGroupTimelockConfigV2(t), + }, + ), + ) + require.NoError(t, err) + + // with MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // propose new Aggregator + commonChangesets.Configure( + changeset.ProposeAggregatorChangeset, + types.ProposeConfirmAggregatorConfig{ + ChainSelector: chainSelector, + ProxyAddress: common.HexToAddress(proxyAddress), + NewAggregatorAddress: common.HexToAddress("0x124"), + }, + ), + // transfer proxy ownership to timelock + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.TransferToMCMSWithTimelockV2), + commonChangesets.TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chainSelector: {common.HexToAddress(proxyAddress)}, + }, + MinDelay: 0, + }, + ), + // confirm from timelock + commonChangesets.Configure( + changeset.ConfirmAggregatorChangeset, + types.ProposeConfirmAggregatorConfig{ + ChainSelector: chainSelector, + ProxyAddress: common.HexToAddress(proxyAddress), + NewAggregatorAddress: common.HexToAddress("0x124"), + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + ) + require.NoError(t, err) +} diff --git a/deployment/data-feeds/changeset/deploy.go b/deployment/data-feeds/changeset/deploy.go new file mode 100644 index 00000000000..b30f3acd555 --- /dev/null +++ b/deployment/data-feeds/changeset/deploy.go @@ -0,0 +1,78 @@ +package changeset + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + proxy "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/aggregator_proxy" + cache "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/data_feeds_cache" +) + +func DeployCache(chain deployment.Chain, labels []string) (*types.DeployCacheResponse, error) { + cacheAddr, tx, cacheContract, err := cache.DeployDataFeedsCache(chain.DeployerKey, chain.Client) + if err != nil { + return nil, fmt.Errorf("failed to deploy DataFeedsCache: %w", err) + } + + _, err = chain.Confirm(tx) + if err != nil { + return nil, fmt.Errorf("failed to confirm DataFeedsCache: %w", err) + } + + tvStr, err := cacheContract.TypeAndVersion(&bind.CallOpts{}) + if err != nil { + return nil, fmt.Errorf("failed to get type and version: %w", err) + } + + tv, err := deployment.TypeAndVersionFromString(tvStr) + if err != nil { + return nil, fmt.Errorf("failed to parse type and version from %s: %w", tvStr, err) + } + + for _, label := range labels { + tv.Labels.Add(label) + } + + resp := &types.DeployCacheResponse{ + Address: cacheAddr, + Tx: tx.Hash(), + Tv: tv, + Contract: cacheContract, + } + return resp, nil +} + +func DeployAggregatorProxy(chain deployment.Chain, aggregator common.Address, accessController common.Address, labels []string) (*types.DeployProxyResponse, error) { + proxyAddr, tx, proxyContract, err := proxy.DeployAggregatorProxy(chain.DeployerKey, chain.Client, aggregator, accessController) + if err != nil { + return nil, fmt.Errorf("failed to deploy AggregatorProxy: %w", err) + } + + _, err = chain.Confirm(tx) + if err != nil { + return nil, fmt.Errorf("failed to confirm AggregatorProxy: %w", err) + } + + // AggregatorProxy contract doesn't implement typeAndVersion interface, so we have to set it manually + tvStr := "AggregatorProxy 1.0.0" + tv, err := deployment.TypeAndVersionFromString(tvStr) + if err != nil { + return nil, fmt.Errorf("failed to parse type and version from %s: %w", tvStr, err) + } + + for _, label := range labels { + tv.Labels.Add(label) + } + + resp := &types.DeployProxyResponse{ + Address: proxyAddr, + Tx: tx.Hash(), + Tv: tv, + Contract: proxyContract, + } + return resp, nil +} diff --git a/deployment/data-feeds/changeset/deploy_aggregator_proxy.go b/deployment/data-feeds/changeset/deploy_aggregator_proxy.go new file mode 100644 index 00000000000..7b555d63d9e --- /dev/null +++ b/deployment/data-feeds/changeset/deploy_aggregator_proxy.go @@ -0,0 +1,70 @@ +package changeset + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" +) + +// DeployAggregatorProxyChangeset deploys an AggregatorProxy contract on the given chains. It uses the address of DataFeedsCache contract +// from addressbook to set it in the AggregatorProxy constructor. Returns a new addressbook with deploy AggregatorProxy contract addresses. +var DeployAggregatorProxyChangeset = deployment.CreateChangeSet(deployAggregatorProxyLogic, deployAggregatorProxyPrecondition) + +func deployAggregatorProxyLogic(env deployment.Environment, c types.DeployAggregatorProxyConfig) (deployment.ChangesetOutput, error) { + lggr := env.Logger + ab := deployment.NewMemoryAddressBook() + + for index, chainSelector := range c.ChainsToDeploy { + chain := env.Chains[chainSelector] + addressMap, _ := env.ExistingAddresses.AddressesForChain(chainSelector) + + var dataFeedsCacheAddress string + cacheTV := deployment.NewTypeAndVersion(DataFeedsCache, deployment.Version1_0_0) + cacheTV.Labels.Add("data-feeds") + for addr, tv := range addressMap { + if tv.String() == cacheTV.String() { + dataFeedsCacheAddress = addr + } + } + + if dataFeedsCacheAddress == "" { + return deployment.ChangesetOutput{}, fmt.Errorf("DataFeedsCache contract address not found in addressbook for chain %d", chainSelector) + } + + proxyResponse, err := DeployAggregatorProxy(chain, common.HexToAddress(dataFeedsCacheAddress), c.AccessController[index], c.Labels) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to deploy AggregatorProxy: %w", err) + } + + lggr.Infof("Deployed %s chain selector %d addr %s", proxyResponse.Tv.String(), chain.Selector, proxyResponse.Address.String()) + + err = ab.Save(chain.Selector, proxyResponse.Address.String(), proxyResponse.Tv) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to save AggregatorProxy: %w", err) + } + } + return deployment.ChangesetOutput{AddressBook: ab}, nil +} + +func deployAggregatorProxyPrecondition(env deployment.Environment, c types.DeployAggregatorProxyConfig) error { + if len(c.AccessController) != len(c.ChainsToDeploy) { + return errors.New("AccessController addresses must be provided for each chain to deploy") + } + + for _, chainSelector := range c.ChainsToDeploy { + _, ok := env.Chains[chainSelector] + if !ok { + return errors.New("chain not found in environment") + } + _, err := env.ExistingAddresses.AddressesForChain(chainSelector) + if err != nil { + return fmt.Errorf("failed to get addessbook for chain %d: %w", chainSelector, err) + } + } + + return nil +} diff --git a/deployment/data-feeds/changeset/deploy_aggregator_proxy_test.go b/deployment/data-feeds/changeset/deploy_aggregator_proxy_test.go new file mode 100644 index 00000000000..23c062e1de2 --- /dev/null +++ b/deployment/data-feeds/changeset/deploy_aggregator_proxy_test.go @@ -0,0 +1,52 @@ +package changeset + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestAggregatorProxy(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 2, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + resp, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + commonChangesets.Configure( + DeployAggregatorProxyChangeset, + types.DeployAggregatorProxyConfig{ + ChainsToDeploy: []uint64{chainSelector}, + AccessController: []common.Address{common.HexToAddress("0x")}, + }, + ), + ) + + require.NoError(t, err) + require.NotNil(t, resp) + + addrs, err := resp.ExistingAddresses.AddressesForChain(chainSelector) + require.NoError(t, err) + require.Len(t, addrs, 2) // AggregatorProxy and DataFeedsCache +} diff --git a/deployment/data-feeds/changeset/deploy_cache.go b/deployment/data-feeds/changeset/deploy_cache.go new file mode 100644 index 00000000000..38bc5619f5d --- /dev/null +++ b/deployment/data-feeds/changeset/deploy_cache.go @@ -0,0 +1,44 @@ +package changeset + +import ( + "errors" + "fmt" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" +) + +// DeployCacheChangeset deploys the DataFeedsCache contract to the specified chains +// Returns a new addressbook with deployed DataFeedsCache contracts +var DeployCacheChangeset = deployment.CreateChangeSet(deployCacheLogic, deployCachePrecondition) + +func deployCacheLogic(env deployment.Environment, c types.DeployConfig) (deployment.ChangesetOutput, error) { + lggr := env.Logger + ab := deployment.NewMemoryAddressBook() + for _, chainSelector := range c.ChainsToDeploy { + chain := env.Chains[chainSelector] + cacheResponse, err := DeployCache(chain, c.Labels) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to deploy DataFeedsCache: %w", err) + } + lggr.Infof("Deployed %s chain selector %d addr %s", cacheResponse.Tv.String(), chain.Selector, cacheResponse.Address.String()) + + err = ab.Save(chain.Selector, cacheResponse.Address.String(), cacheResponse.Tv) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to save DataFeedsCache: %w", err) + } + } + + return deployment.ChangesetOutput{AddressBook: ab}, nil +} + +func deployCachePrecondition(env deployment.Environment, c types.DeployConfig) error { + for _, chainSelector := range c.ChainsToDeploy { + _, ok := env.Chains[chainSelector] + if !ok { + return errors.New("chain not found in environment") + } + } + + return nil +} diff --git a/deployment/data-feeds/changeset/deploy_cache_test.go b/deployment/data-feeds/changeset/deploy_cache_test.go new file mode 100644 index 00000000000..83c0442973e --- /dev/null +++ b/deployment/data-feeds/changeset/deploy_cache_test.go @@ -0,0 +1,43 @@ +package changeset_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestDeployCache(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 2, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + resp, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + }, + ), + ) + require.NoError(t, err) + require.NotNil(t, resp) + + addrs, err := resp.ExistingAddresses.AddressesForChain(chainSelector) + require.NoError(t, err) + require.Len(t, addrs, 1) +} diff --git a/deployment/data-feeds/changeset/import_to_addressbook.go b/deployment/data-feeds/changeset/import_to_addressbook.go new file mode 100644 index 00000000000..08628568ee9 --- /dev/null +++ b/deployment/data-feeds/changeset/import_to_addressbook.go @@ -0,0 +1,58 @@ +package changeset + +import ( + "errors" + "fmt" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/shared" +) + +// ImportToAddressbookChangeset is a changeset that reads already deployed contract addresses from input file +// and saves them to the address book. Returns a new addressbook with the imported addresses. +var ImportToAddressbookChangeset = deployment.CreateChangeSet(importToAddressbookLogic, importToAddressbookPrecondition) + +type AddressesSchema struct { + Address string `json:"address"` + TypeAndVersion deployment.TypeAndVersion `json:"typeAndVersion"` + Label string `json:"label"` +} + +func importToAddressbookLogic(env deployment.Environment, c types.ImportToAddressbookConfig) (deployment.ChangesetOutput, error) { + ab := deployment.NewMemoryAddressBook() + + addresses, _ := shared.LoadJSON[[]*AddressesSchema](c.InputFileName, c.InputFS) + + for _, address := range addresses { + address.TypeAndVersion.AddLabel(address.Label) + err := ab.Save( + c.ChainSelector, + address.Address, + address.TypeAndVersion, + ) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to save address %s: %w", address.Address, err) + } + } + + return deployment.ChangesetOutput{AddressBook: ab}, nil +} + +func importToAddressbookPrecondition(env deployment.Environment, c types.ImportToAddressbookConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + if c.InputFileName == "" { + return errors.New("input file name is required") + } + + _, err := shared.LoadJSON[[]*AddressesSchema](c.InputFileName, c.InputFS) + if err != nil { + return fmt.Errorf("failed to load addresses input file: %w", err) + } + + return nil +} diff --git a/deployment/data-feeds/changeset/import_to_addressbook_test.go b/deployment/data-feeds/changeset/import_to_addressbook_test.go new file mode 100644 index 00000000000..6df865f7c31 --- /dev/null +++ b/deployment/data-feeds/changeset/import_to_addressbook_test.go @@ -0,0 +1,48 @@ +package changeset_test + +import ( + "embed" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +//go:embed testdata/* +var testFS embed.FS + +func TestImportToAddressbook(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + resp, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + changeset.ImportToAddressbookChangeset, + types.ImportToAddressbookConfig{ + ChainSelector: chainSelector, + InputFileName: "testdata/import_addresses.json", + InputFS: testFS, + }, + ), + ) + + require.NoError(t, err) + require.NotNil(t, resp) + tv, _ := resp.ExistingAddresses.AddressesForChain(chainSelector) + require.Len(t, tv, 2) +} diff --git a/deployment/data-feeds/changeset/migrate_feeds.go b/deployment/data-feeds/changeset/migrate_feeds.go new file mode 100644 index 00000000000..922c29df846 --- /dev/null +++ b/deployment/data-feeds/changeset/migrate_feeds.go @@ -0,0 +1,101 @@ +package changeset + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/shared" +) + +// MigrateFeedsChangeset Migrates feeds to DataFeedsCache contract. +// 1. It reads the existing Aggregator Proxy contract addresses from the input file and saves them to the address book. +// 2. It reads the data ids and descriptions from the input file and sets the feed config on the DataFeedsCache contract. +// Returns a new addressbook with the deployed AggregatorProxy addresses. +var MigrateFeedsChangeset = deployment.CreateChangeSet(migrateFeedsLogic, migrateFeedsPrecondition) + +type MigrationSchema struct { + Address string `json:"address"` + TypeAndVersion deployment.TypeAndVersion `json:"typeAndVersion"` + FeedID string `json:"feedId"` // without 0x prefix + Description string `json:"description"` +} + +func migrateFeedsLogic(env deployment.Environment, c types.MigrationConfig) (deployment.ChangesetOutput, error) { + state, _ := LoadOnchainState(env) + chain := env.Chains[c.ChainSelector] + chainState := state.Chains[c.ChainSelector] + contract := chainState.DataFeedsCache[c.CacheAddress] + ab := deployment.NewMemoryAddressBook() + + proxies, _ := shared.LoadJSON[[]*MigrationSchema](c.InputFileName, c.InputFS) + + dataIDs := make([][16]byte, len(proxies)) + addresses := make([]common.Address, len(proxies)) + descriptions := make([]string, len(proxies)) + for i, proxy := range proxies { + dataIDBytes16, err := shared.ConvertHexToBytes16(proxy.FeedID) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("cannot convert hex to bytes %s: %w", proxy.FeedID, err) + } + + dataIDs[i] = dataIDBytes16 + addresses[i] = common.HexToAddress(proxy.Address) + descriptions[i] = proxy.Description + + proxy.TypeAndVersion.AddLabel(proxy.Description) + err = ab.Save( + c.ChainSelector, + proxy.Address, + proxy.TypeAndVersion, + ) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to save address %s: %w", proxy.Address, err) + } + } + + // Set the feed config + tx, err := contract.SetDecimalFeedConfigs(chain.DeployerKey, dataIDs, descriptions, c.WorkflowMetadata) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to set feed config %w", err) + } + + _, err = chain.Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", tx.Hash().String(), err) + } + + // Set the proxy to dataId mapping + tx, err = contract.UpdateDataIdMappingsForProxies(chain.DeployerKey, addresses, dataIDs) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to update feed proxy mapping %w", err) + } + + _, err = chain.Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", tx.Hash().String(), err) + } + + return deployment.ChangesetOutput{AddressBook: ab}, nil +} + +func migrateFeedsPrecondition(env deployment.Environment, c types.MigrationConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + _, err := shared.LoadJSON[[]*MigrationSchema](c.InputFileName, c.InputFS) + if err != nil { + return fmt.Errorf("failed to load addresses input file: %w", err) + } + + if len(c.WorkflowMetadata) == 0 { + return errors.New("workflow metadata is required") + } + + return ValidateCacheForChain(env, c.ChainSelector, c.CacheAddress) +} diff --git a/deployment/data-feeds/changeset/migrate_feeds_test.go b/deployment/data-feeds/changeset/migrate_feeds_test.go new file mode 100644 index 00000000000..47f772a348d --- /dev/null +++ b/deployment/data-feeds/changeset/migrate_feeds_test.go @@ -0,0 +1,79 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/shared" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" + cache "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/data_feeds_cache" +) + +func TestMigrateFeeds(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + newEnv, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + ) + require.NoError(t, err) + + cacheAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "DataFeedsCache") + require.NoError(t, err) + + resp, err := commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(env.Chains[chainSelector].DeployerKey.From.Hex()), + IsAdmin: true, + }, + ), + commonChangesets.Configure( + changeset.MigrateFeedsChangeset, + types.MigrationConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + InputFileName: "testdata/migrate_feeds.json", + InputFS: testFS, + WorkflowMetadata: []cache.DataFeedsCacheWorkflowMetadata{ + cache.DataFeedsCacheWorkflowMetadata{ + AllowedSender: common.HexToAddress("0x22"), + AllowedWorkflowOwner: common.HexToAddress("0x33"), + AllowedWorkflowName: shared.HashedWorkflowName("test"), + }, + }, + }, + ), + ) + require.NoError(t, err) + require.NotNil(t, resp) + addresses, err := resp.ExistingAddresses.AddressesForChain(chainSelector) + require.NoError(t, err) + require.Len(t, addresses, 3) // DataFeedsCache and two migrated proxies +} diff --git a/deployment/data-feeds/changeset/new_feed_with_proxy.go b/deployment/data-feeds/changeset/new_feed_with_proxy.go new file mode 100644 index 00000000000..a56278db53a --- /dev/null +++ b/deployment/data-feeds/changeset/new_feed_with_proxy.go @@ -0,0 +1,144 @@ +package changeset + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + mcmslib "github.com/smartcontractkit/mcms" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/changeset" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" +) + +// NewFeedWithProxyChangeset configures a new feed with a proxy +// 1. Deploys AggregatorProxy contract for given chainselector +// 2. Proposes and confirms DataFeedsCache contract as an aggregator on AggregatorProxy +// 3. Creates an MCMS proposal to transfer the ownership of AggregatorProxy contract to timelock +// 4. Creates a proposal to set a feed config on DataFeedsCache contract +// 5. Creates a proposal to set a feed proxy mapping on DataFeedsCache contract +// Returns a new addressbook with the new AggregatorProxy contract address and 3 MCMS proposals +var NewFeedWithProxyChangeset = deployment.CreateChangeSet(newFeedWithProxyLogic, newFeedWithProxyPrecondition) + +func newFeedWithProxyLogic(env deployment.Environment, c types.NewFeedWithProxyConfig) (deployment.ChangesetOutput, error) { + chain := env.Chains[c.ChainSelector] + state, _ := LoadOnchainState(env) + chainState := state.Chains[c.ChainSelector] + + // Deploy AggregatorProxy contract with deployer key + proxyConfig := types.DeployAggregatorProxyConfig{ + ChainsToDeploy: []uint64{c.ChainSelector}, + AccessController: []common.Address{c.AccessController}, + Labels: c.Labels, + } + newEnv, err := DeployAggregatorProxyChangeset.Apply(env, proxyConfig) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to execute DeployAggregatorProxyChangeset: %w", err) + } + + proxyAddress, err := deployment.SearchAddressBook(newEnv.AddressBook, c.ChainSelector, "AggregatorProxy") + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("AggregatorProxy not present in addressbook: %w", err) + } + + addressMap, _ := env.ExistingAddresses.AddressesForChain(c.ChainSelector) + var dataFeedsCacheAddress string + cacheTV := deployment.NewTypeAndVersion(DataFeedsCache, deployment.Version1_0_0) + cacheTV.Labels.Add("data-feeds") + for addr, tv := range addressMap { + if tv.String() == cacheTV.String() { + dataFeedsCacheAddress = addr + } + } + + dataFeedsCache := chainState.DataFeedsCache[common.HexToAddress(dataFeedsCacheAddress)] + if dataFeedsCache == nil { + return deployment.ChangesetOutput{}, errors.New("DataFeedsCache contract not found in onchain state") + } + + // Propose and confirm DataFeedsCache contract as an aggregator on AggregatorProxy + proposeAggregatorConfig := types.ProposeConfirmAggregatorConfig{ + ChainSelector: c.ChainSelector, + ProxyAddress: common.HexToAddress(proxyAddress), + NewAggregatorAddress: common.HexToAddress(dataFeedsCacheAddress), + } + + _, err = ProposeAggregatorChangeset.Apply(env, proposeAggregatorConfig) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to execute ProposeAggregatorChangeset: %w", err) + } + + _, err = ConfirmAggregatorChangeset.Apply(env, proposeAggregatorConfig) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to execute ConfirmAggregatorChangeset: %w", err) + } + + // Create an MCMS proposal to transfer the ownership of AggregatorProxy contract to timelock and set the feed configs + // We don't use the existing changesets so that we can batch the transactions into a single MCMS proposal + + // transfer proxy ownership + timelockAddr, _ := deployment.SearchAddressBook(env.ExistingAddresses, c.ChainSelector, commonTypes.RBACTimelock) + _, proxyContract, err := changeset.LoadOwnableContract(common.HexToAddress(proxyAddress), chain.Client) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load proxy contract %w", err) + } + tx, err := proxyContract.TransferOwnership(chain.DeployerKey, common.HexToAddress(timelockAddr)) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create transfer ownership tx %w", err) + } + _, err = chain.Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", tx.Hash().String(), err) + } + + // accept proxy ownership proposal + acceptProxyOwnerShipTx, err := proxyContract.AcceptOwnership(deployment.SimTransactOpts()) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create accept transfer ownership tx %w", err) + } + + // set feed config proposal + setFeedConfigTx, err := dataFeedsCache.SetDecimalFeedConfigs(deployment.SimTransactOpts(), [][16]byte{c.DataID}, []string{c.Description}, c.WorkflowMetadata) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to set feed config %w", err) + } + + // set feed proxy mapping proposal + setProxyMappingTx, err := dataFeedsCache.UpdateDataIdMappingsForProxies(deployment.SimTransactOpts(), []common.Address{common.HexToAddress(proxyAddress)}, [][16]byte{c.DataID}) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to set proxy-dataId mapping %w", err) + } + + txs := []ProposalData{ + { + contract: proxyContract.Address().Hex(), + tx: acceptProxyOwnerShipTx, + }, + { + contract: dataFeedsCache.Address().Hex(), + tx: setFeedConfigTx, + }, + { + contract: dataFeedsCache.Address().Hex(), + tx: setProxyMappingTx, + }, + } + + proposals, err := BuildMCMProposals(env, "accept AggregatorProxy ownership to timelock. set feed config and proxy mapping on cache", c.ChainSelector, txs, c.McmsConfig.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + + return deployment.ChangesetOutput{AddressBook: newEnv.AddressBook, MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposals}}, nil +} + +func newFeedWithProxyPrecondition(env deployment.Environment, c types.NewFeedWithProxyConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + return ValidateMCMSAddresses(env.ExistingAddresses, c.ChainSelector) +} diff --git a/deployment/data-feeds/changeset/new_feed_with_proxy_test.go b/deployment/data-feeds/changeset/new_feed_with_proxy_test.go new file mode 100644 index 00000000000..b1ccc85206c --- /dev/null +++ b/deployment/data-feeds/changeset/new_feed_with_proxy_test.go @@ -0,0 +1,111 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/shared" + cache "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/data_feeds_cache" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestNewFeedWithProxy(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + newEnv, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.DeployMCMSWithTimelockV2), + map[uint64]commonTypes.MCMSWithTimelockConfigV2{ + chainSelector: proposalutils.SingleGroupTimelockConfigV2(t), + }, + ), + ) + require.NoError(t, err) + + cacheAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "DataFeedsCache") + require.NoError(t, err) + + timeLockAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "RBACTimelock") + require.NoError(t, err) + + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // Set the admin to the timelock + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(timeLockAddress), + IsAdmin: true, + }, + ), + // Transfer cache ownership to MCMS + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.TransferToMCMSWithTimelockV2), + commonChangesets.TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chainSelector: {common.HexToAddress(cacheAddress)}, + }, + MinDelay: 0, + }, + ), + ) + require.NoError(t, err) + + dataid, _ := shared.ConvertHexToBytes16("01bb0467f50003040000000000000000") + + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.NewFeedWithProxyChangeset, + types.NewFeedWithProxyConfig{ + ChainSelector: chainSelector, + AccessController: common.HexToAddress("0x00"), + DataID: dataid, + Description: "test2", + WorkflowMetadata: []cache.DataFeedsCacheWorkflowMetadata{ + cache.DataFeedsCacheWorkflowMetadata{ + AllowedSender: common.HexToAddress("0x22"), + AllowedWorkflowOwner: common.HexToAddress("0x33"), + AllowedWorkflowName: shared.HashedWorkflowName("test"), + }, + }, + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + ) + require.NoError(t, err) + + addrs, err := newEnv.ExistingAddresses.AddressesForChain(chainSelector) + require.NoError(t, err) + // AggregatorProxy, DataFeedsCache, CallProxy, RBACTimelock, ProposerManyChainMultiSig, BypasserManyChainMultiSig, CancellerManyChainMultiSig + require.Len(t, addrs, 7) +} diff --git a/deployment/data-feeds/changeset/proposal.go b/deployment/data-feeds/changeset/proposal.go new file mode 100644 index 00000000000..d896768b976 --- /dev/null +++ b/deployment/data-feeds/changeset/proposal.go @@ -0,0 +1,64 @@ +package changeset + +import ( + "encoding/json" + "time" + + gethTypes "github.com/ethereum/go-ethereum/core/types" + mcmslib "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/sdk/evm" + mcmstypes "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" +) + +type ProposalData struct { + contract string + tx *gethTypes.Transaction +} + +func BuildMCMProposals(env deployment.Environment, description string, chainSelector uint64, pd []ProposalData, minDelay time.Duration) (*mcmslib.TimelockProposal, error) { + state, _ := LoadOnchainState(env) + chain := env.Chains[chainSelector] + chainState := state.Chains[chainSelector] + + var transactions []mcmstypes.Transaction + for _, proposal := range pd { + transactions = append(transactions, mcmstypes.Transaction{ + To: proposal.contract, + Data: proposal.tx.Data(), + AdditionalFields: json.RawMessage(`{"value": 0}`), + }) + } + + ops := &mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(chainSelector), + Transactions: transactions, + } + + timelocksPerChain := map[uint64]string{ + chainSelector: chainState.Timelock.Address().Hex(), + } + proposerMCMSes := map[uint64]string{ + chainSelector: chainState.ProposerMcm.Address().Hex(), + } + + inspectorPerChain := map[uint64]sdk.Inspector{} + inspectorPerChain[chainSelector] = evm.NewInspector(chain.Client) + + proposal, err := proposalutils.BuildProposalFromBatchesV2( + env, + timelocksPerChain, + proposerMCMSes, + inspectorPerChain, + []mcmstypes.BatchOperation{*ops}, + description, + minDelay, + ) + if err != nil { + return nil, err + } + return proposal, err +} diff --git a/deployment/data-feeds/changeset/propose_aggregator.go b/deployment/data-feeds/changeset/propose_aggregator.go new file mode 100644 index 00000000000..72a1ddc0325 --- /dev/null +++ b/deployment/data-feeds/changeset/propose_aggregator.go @@ -0,0 +1,68 @@ +package changeset + +import ( + "fmt" + + mcmslib "github.com/smartcontractkit/mcms" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + proxy "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/aggregator_proxy" +) + +// ProposeAggregatorChangeset is a changeset that proposes a new aggregator on existing AggregatorProxy contract +// This changeset may return a timelock proposal if the MCMS config is provided, otherwise it will execute the transaction with the deployer key. +var ProposeAggregatorChangeset = deployment.CreateChangeSet(proposeAggregatorLogic, proposeAggregatorPrecondition) + +func proposeAggregatorLogic(env deployment.Environment, c types.ProposeConfirmAggregatorConfig) (deployment.ChangesetOutput, error) { + chain := env.Chains[c.ChainSelector] + + aggregatorProxy, err := proxy.NewAggregatorProxy(c.ProxyAddress, chain.Client) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load AggregatorProxy: %w", err) + } + + txOpt := chain.DeployerKey + if c.McmsConfig != nil { + txOpt = deployment.SimTransactOpts() + } + + tx, err := aggregatorProxy.ProposeAggregator(txOpt, c.NewAggregatorAddress) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to execute ProposeAggregator: %w", err) + } + + if c.McmsConfig != nil { + proposal, err := BuildMCMProposals(env, "proposal to propose a new aggregator", c.ChainSelector, []ProposalData{ + { + contract: aggregatorProxy.Address().Hex(), + tx: tx, + }, + }, c.McmsConfig.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil + } + _, err = chain.Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", tx.Hash().String(), err) + } + + return deployment.ChangesetOutput{}, nil +} + +func proposeAggregatorPrecondition(env deployment.Environment, c types.ProposeConfirmAggregatorConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + if c.McmsConfig != nil { + if err := ValidateMCMSAddresses(env.ExistingAddresses, c.ChainSelector); err != nil { + return err + } + } + + return nil +} diff --git a/deployment/data-feeds/changeset/propose_aggregator_test.go b/deployment/data-feeds/changeset/propose_aggregator_test.go new file mode 100644 index 00000000000..b450173be1f --- /dev/null +++ b/deployment/data-feeds/changeset/propose_aggregator_test.go @@ -0,0 +1,101 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestProposeAggregator(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + // without MCMS + newEnv, err := commonChangesets.Apply(t, env, nil, + // Deploy cache and aggregator proxy + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + commonChangesets.Configure( + changeset.DeployAggregatorProxyChangeset, + types.DeployAggregatorProxyConfig{ + ChainsToDeploy: []uint64{chainSelector}, + AccessController: []common.Address{common.HexToAddress("0x")}, + }, + ), + ) + require.NoError(t, err) + + proxyAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "AggregatorProxy") + require.NoError(t, err) + + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // Propose a new aggregator + commonChangesets.Configure( + changeset.ProposeAggregatorChangeset, + types.ProposeConfirmAggregatorConfig{ + ChainSelector: chainSelector, + ProxyAddress: common.HexToAddress(proxyAddress), + NewAggregatorAddress: common.HexToAddress("0x123"), + }, + ), + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.DeployMCMSWithTimelockV2), + map[uint64]commonTypes.MCMSWithTimelockConfigV2{ + chainSelector: proposalutils.SingleGroupTimelockConfigV2(t), + }, + ), + ) + require.NoError(t, err) + + // with MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // transfer proxy ownership to timelock + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.TransferToMCMSWithTimelockV2), + commonChangesets.TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chainSelector: {common.HexToAddress(proxyAddress)}, + }, + MinDelay: 0, + }, + ), + commonChangesets.Configure( + changeset.ProposeAggregatorChangeset, + types.ProposeConfirmAggregatorConfig{ + ChainSelector: chainSelector, + ProxyAddress: common.HexToAddress(proxyAddress), + NewAggregatorAddress: common.HexToAddress("0x123"), + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + ) + require.NoError(t, err) +} diff --git a/deployment/data-feeds/changeset/remove_dataid_proxy_mapping.go b/deployment/data-feeds/changeset/remove_dataid_proxy_mapping.go new file mode 100644 index 00000000000..525bb053080 --- /dev/null +++ b/deployment/data-feeds/changeset/remove_dataid_proxy_mapping.go @@ -0,0 +1,69 @@ +package changeset + +import ( + "errors" + "fmt" + + mcmslib "github.com/smartcontractkit/mcms" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" +) + +// RemoveFeedProxyMappingChangeset is a changeset that only removes a feed-aggregator proxy mapping from DataFeedsCache contract. +// This changeset may return a timelock proposal if the MCMS config is provided, otherwise it will execute the transaction with the deployer key. +var RemoveFeedProxyMappingChangeset = deployment.CreateChangeSet(removeFeedProxyMappingLogic, removeFeedFeedProxyMappingPrecondition) + +func removeFeedProxyMappingLogic(env deployment.Environment, c types.RemoveFeedProxyConfig) (deployment.ChangesetOutput, error) { + state, _ := LoadOnchainState(env) + chain := env.Chains[c.ChainSelector] + chainState := state.Chains[c.ChainSelector] + contract := chainState.DataFeedsCache[c.CacheAddress] + + txOpt := chain.DeployerKey + if c.McmsConfig != nil { + txOpt = deployment.SimTransactOpts() + } + + tx, err := contract.RemoveDataIdMappingsForProxies(txOpt, c.ProxyAddresses) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to remove feed proxy mapping %w", err) + } + + if c.McmsConfig != nil { + proposal, err := BuildMCMProposals(env, "proposal to remove a feed proxy mapping from cache", c.ChainSelector, []ProposalData{ + { + contract: contract.Address().Hex(), + tx: tx, + }, + }, c.McmsConfig.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil + } + _, err = chain.Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", tx.Hash().String(), err) + } + + return deployment.ChangesetOutput{}, nil +} + +func removeFeedFeedProxyMappingPrecondition(env deployment.Environment, c types.RemoveFeedProxyConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + if len(c.ProxyAddresses) == 0 { + return errors.New("proxy addresses must not be empty") + } + if c.McmsConfig != nil { + if err := ValidateMCMSAddresses(env.ExistingAddresses, c.ChainSelector); err != nil { + return err + } + } + + return ValidateCacheForChain(env, c.ChainSelector, c.CacheAddress) +} diff --git a/deployment/data-feeds/changeset/remove_dataid_proxy_mapping_test.go b/deployment/data-feeds/changeset/remove_dataid_proxy_mapping_test.go new file mode 100644 index 00000000000..e01040dd633 --- /dev/null +++ b/deployment/data-feeds/changeset/remove_dataid_proxy_mapping_test.go @@ -0,0 +1,144 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/shared" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestRemoveFeedProxyMapping(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + newEnv, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.DeployMCMSWithTimelockV2), + map[uint64]commonTypes.MCMSWithTimelockConfigV2{ + chainSelector: proposalutils.SingleGroupTimelockConfigV2(t), + }, + ), + ) + require.NoError(t, err) + + cacheAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "DataFeedsCache") + require.NoError(t, err) + + dataid, _ := shared.ConvertHexToBytes16("01bb0467f50003040000000000000000") + + // without MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // set the feed admin, only admin can perform set/remove operations + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(env.Chains[chainSelector].DeployerKey.From.Hex()), + IsAdmin: true, + }, + ), + // set the feed proxy mapping + commonChangesets.Configure( + changeset.UpdateDataIDProxyChangeset, + types.UpdateDataIDProxyConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + ProxyAddresses: []common.Address{common.HexToAddress("0x11")}, + DataIDs: [][16]byte{dataid}, + }, + ), + // remove the feed proxy mapping + commonChangesets.Configure( + changeset.RemoveFeedProxyMappingChangeset, + types.RemoveFeedProxyConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + ProxyAddresses: []common.Address{common.HexToAddress("0x11")}, + }, + ), + ) + require.NoError(t, err) + + // with MCMS + timeLockAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "RBACTimelock") + require.NoError(t, err) + + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // Set the admin to the timelock + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(timeLockAddress), + IsAdmin: true, + }, + ), + // Transfer cache ownership to MCMS + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.TransferToMCMSWithTimelockV2), + commonChangesets.TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chainSelector: {common.HexToAddress(cacheAddress)}, + }, + MinDelay: 0, + }, + ), + ) + require.NoError(t, err) + + // Set and remove the feed config with MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.UpdateDataIDProxyChangeset, + types.UpdateDataIDProxyConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + ProxyAddresses: []common.Address{common.HexToAddress("0x11")}, + DataIDs: [][16]byte{dataid}, + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + commonChangesets.Configure( + changeset.RemoveFeedProxyMappingChangeset, + types.RemoveFeedProxyConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + ProxyAddresses: []common.Address{common.HexToAddress("0x11")}, + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + ) + require.NoError(t, err) +} diff --git a/deployment/data-feeds/changeset/remove_feed.go b/deployment/data-feeds/changeset/remove_feed.go new file mode 100644 index 00000000000..6b140c2d717 --- /dev/null +++ b/deployment/data-feeds/changeset/remove_feed.go @@ -0,0 +1,92 @@ +package changeset + +import ( + "errors" + "fmt" + + mcmslib "github.com/smartcontractkit/mcms" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" +) + +// RemoveFeedChangeset is a changeset that removes a feed configuration and aggregator proxy mapping from DataFeedsCache contract. +// This changeset may return a timelock proposal if the MCMS config is provided, otherwise it will execute the transactions with the deployer key. +var RemoveFeedChangeset = deployment.CreateChangeSet(removeFeedLogic, removeFeedPrecondition) + +func removeFeedLogic(env deployment.Environment, c types.RemoveFeedConfig) (deployment.ChangesetOutput, error) { + state, _ := LoadOnchainState(env) + chain := env.Chains[c.ChainSelector] + chainState := state.Chains[c.ChainSelector] + contract := chainState.DataFeedsCache[c.CacheAddress] + + txOpt := chain.DeployerKey + if c.McmsConfig != nil { + txOpt = deployment.SimTransactOpts() + } + + // remove the feed config + removeConfigTx, err := contract.RemoveFeedConfigs(txOpt, c.DataIDs) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to remove feed config %w", err) + } + + if c.McmsConfig == nil { + _, err = chain.Confirm(removeConfigTx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", removeConfigTx.Hash().String(), err) + } + } + + // remove from proxy mapping + removeProxyMappingTx, err := contract.RemoveDataIdMappingsForProxies(txOpt, c.ProxyAddresses) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to remove proxy mapping %w", err) + } + + if c.McmsConfig == nil { + _, err = chain.Confirm(removeProxyMappingTx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", removeConfigTx.Hash().String(), err) + } + return deployment.ChangesetOutput{}, nil + } + + txs := []ProposalData{ + { + contract: contract.Address().Hex(), + tx: removeConfigTx, + }, + { + contract: contract.Address().Hex(), + tx: removeProxyMappingTx, + }, + } + proposal, err := BuildMCMProposals(env, "proposal to remove a feed from cache", c.ChainSelector, txs, c.McmsConfig.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil +} + +func removeFeedPrecondition(env deployment.Environment, c types.RemoveFeedConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + if (len(c.DataIDs) == 0) || (len(c.ProxyAddresses) == 0) { + return errors.New("dataIDs and proxy addresses must not be empty") + } + if len(c.DataIDs) != len(c.ProxyAddresses) { + return errors.New("dataIDs and proxy addresses must have the same length") + } + + if c.McmsConfig != nil { + if err := ValidateMCMSAddresses(env.ExistingAddresses, c.ChainSelector); err != nil { + return err + } + } + + return ValidateCacheForChain(env, c.ChainSelector, c.CacheAddress) +} diff --git a/deployment/data-feeds/changeset/remove_feed_config.go b/deployment/data-feeds/changeset/remove_feed_config.go new file mode 100644 index 00000000000..eb3b813f2f1 --- /dev/null +++ b/deployment/data-feeds/changeset/remove_feed_config.go @@ -0,0 +1,64 @@ +package changeset + +import ( + "errors" + "fmt" + + mcmslib "github.com/smartcontractkit/mcms" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" +) + +// RemoveFeedConfigChangeset is a changeset that only removes a feed configuration from DataFeedsCache contract. +// This changeset may return a timelock proposal if the MCMS config is provided, otherwise it will execute the transaction with the deployer key. +var RemoveFeedConfigChangeset = deployment.CreateChangeSet(removeFeedConfigLogic, removeFeedConfigPrecondition) + +func removeFeedConfigLogic(env deployment.Environment, c types.RemoveFeedConfigCSConfig) (deployment.ChangesetOutput, error) { + state, _ := LoadOnchainState(env) + chain := env.Chains[c.ChainSelector] + chainState := state.Chains[c.ChainSelector] + contract := chainState.DataFeedsCache[c.CacheAddress] + + txOpt := chain.DeployerKey + if c.McmsConfig != nil { + txOpt = deployment.SimTransactOpts() + } + + tx, err := contract.RemoveFeedConfigs(txOpt, c.DataIDs) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to remove feed config %w", err) + } + + if c.McmsConfig != nil { + proposal, err := BuildMCMProposals(env, "proposal to remove a feed config from cache", c.ChainSelector, []ProposalData{ + { + contract: contract.Address().Hex(), + tx: tx, + }, + }, c.McmsConfig.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil + } + _, err = chain.Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", tx.Hash().String(), err) + } + + return deployment.ChangesetOutput{}, nil +} + +func removeFeedConfigPrecondition(env deployment.Environment, c types.RemoveFeedConfigCSConfig) error { + if len(c.DataIDs) == 0 { + return errors.New("dataIDs must not be empty") + } + if c.McmsConfig != nil { + if err := ValidateMCMSAddresses(env.ExistingAddresses, c.ChainSelector); err != nil { + return err + } + } + + return ValidateCacheForChain(env, c.ChainSelector, c.CacheAddress) +} diff --git a/deployment/data-feeds/changeset/remove_feed_config_test.go b/deployment/data-feeds/changeset/remove_feed_config_test.go new file mode 100644 index 00000000000..9223e831b8b --- /dev/null +++ b/deployment/data-feeds/changeset/remove_feed_config_test.go @@ -0,0 +1,160 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + cache "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/data_feeds_cache" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/shared" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestRemoveFeedConfig(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + newEnv, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.DeployMCMSWithTimelockV2), + map[uint64]commonTypes.MCMSWithTimelockConfigV2{ + chainSelector: proposalutils.SingleGroupTimelockConfigV2(t), + }, + ), + ) + require.NoError(t, err) + + cacheAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "DataFeedsCache") + require.NoError(t, err) + + dataid, _ := shared.ConvertHexToBytes16("01bb0467f50003040000000000000000") + + // without MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // set the feed admin, only admin can perform set/remove operations + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(env.Chains[chainSelector].DeployerKey.From.Hex()), + IsAdmin: true, + }, + ), + // set the feed config + commonChangesets.Configure( + changeset.SetFeedConfigChangeset, + types.SetFeedDecimalConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + DataIDs: [][16]byte{dataid}, + Descriptions: []string{"test"}, + WorkflowMetadata: []cache.DataFeedsCacheWorkflowMetadata{ + cache.DataFeedsCacheWorkflowMetadata{ + AllowedSender: common.HexToAddress("0x22"), + AllowedWorkflowOwner: common.HexToAddress("0x33"), + AllowedWorkflowName: shared.HashedWorkflowName("test"), + }, + }, + }, + ), + // remove the feed config + commonChangesets.Configure( + changeset.RemoveFeedConfigChangeset, + types.RemoveFeedConfigCSConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + DataIDs: [][16]byte{dataid}, + }, + ), + ) + require.NoError(t, err) + + // with MCMS + timeLockAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "RBACTimelock") + require.NoError(t, err) + + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // Set the admin to the timelock + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(timeLockAddress), + IsAdmin: true, + }, + ), + // Transfer cache ownership to MCMS + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.TransferToMCMSWithTimelockV2), + commonChangesets.TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chainSelector: {common.HexToAddress(cacheAddress)}, + }, + MinDelay: 0, + }, + ), + ) + require.NoError(t, err) + + // Set and remove the feed config with MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.SetFeedConfigChangeset, + types.SetFeedDecimalConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + DataIDs: [][16]byte{dataid}, + Descriptions: []string{"test2"}, + WorkflowMetadata: []cache.DataFeedsCacheWorkflowMetadata{ + cache.DataFeedsCacheWorkflowMetadata{ + AllowedSender: common.HexToAddress("0x22"), + AllowedWorkflowOwner: common.HexToAddress("0x33"), + AllowedWorkflowName: shared.HashedWorkflowName("test"), + }, + }, + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + commonChangesets.Configure( + changeset.RemoveFeedConfigChangeset, + types.RemoveFeedConfigCSConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + DataIDs: [][16]byte{dataid}, + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + ) + require.NoError(t, err) +} diff --git a/deployment/data-feeds/changeset/remove_feed_test.go b/deployment/data-feeds/changeset/remove_feed_test.go new file mode 100644 index 00000000000..6f611af876e --- /dev/null +++ b/deployment/data-feeds/changeset/remove_feed_test.go @@ -0,0 +1,162 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + cache "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/data_feeds_cache" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/shared" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestRemoveFeed(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + newEnv, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.DeployMCMSWithTimelockV2), + map[uint64]commonTypes.MCMSWithTimelockConfigV2{ + chainSelector: proposalutils.SingleGroupTimelockConfigV2(t), + }, + ), + ) + require.NoError(t, err) + + cacheAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "DataFeedsCache") + require.NoError(t, err) + + dataid, _ := shared.ConvertHexToBytes16("01bb0467f50003040000000000000000") + + // without MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // set the feed admin, only admin can perform set/remove operations + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(env.Chains[chainSelector].DeployerKey.From.Hex()), + IsAdmin: true, + }, + ), + // set the feed config + commonChangesets.Configure( + changeset.SetFeedConfigChangeset, + types.SetFeedDecimalConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + DataIDs: [][16]byte{dataid}, + Descriptions: []string{"test"}, + WorkflowMetadata: []cache.DataFeedsCacheWorkflowMetadata{ + cache.DataFeedsCacheWorkflowMetadata{ + AllowedSender: common.HexToAddress("0x22"), + AllowedWorkflowOwner: common.HexToAddress("0x33"), + AllowedWorkflowName: shared.HashedWorkflowName("test"), + }, + }, + }, + ), + // remove the feed config + commonChangesets.Configure( + changeset.RemoveFeedChangeset, + types.RemoveFeedConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + DataIDs: [][16]byte{dataid}, + ProxyAddresses: []common.Address{common.HexToAddress("0x123")}, + }, + ), + ) + require.NoError(t, err) + + // with MCMS + timeLockAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "RBACTimelock") + require.NoError(t, err) + + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // Set the admin to the timelock + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(timeLockAddress), + IsAdmin: true, + }, + ), + // Transfer cache ownership to MCMS + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.TransferToMCMSWithTimelockV2), + commonChangesets.TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chainSelector: {common.HexToAddress(cacheAddress)}, + }, + MinDelay: 0, + }, + ), + ) + require.NoError(t, err) + + // Set and remove the feed config with MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.SetFeedConfigChangeset, + types.SetFeedDecimalConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + DataIDs: [][16]byte{dataid}, + Descriptions: []string{"test2"}, + WorkflowMetadata: []cache.DataFeedsCacheWorkflowMetadata{ + cache.DataFeedsCacheWorkflowMetadata{ + AllowedSender: common.HexToAddress("0x22"), + AllowedWorkflowOwner: common.HexToAddress("0x33"), + AllowedWorkflowName: shared.HashedWorkflowName("test"), + }, + }, + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + commonChangesets.Configure( + changeset.RemoveFeedChangeset, + types.RemoveFeedConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + DataIDs: [][16]byte{dataid}, + ProxyAddresses: []common.Address{common.HexToAddress("0x123")}, + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + ) + require.NoError(t, err) +} diff --git a/deployment/data-feeds/changeset/set_feed_admin.go b/deployment/data-feeds/changeset/set_feed_admin.go new file mode 100644 index 00000000000..7ef083c8e3e --- /dev/null +++ b/deployment/data-feeds/changeset/set_feed_admin.go @@ -0,0 +1,65 @@ +package changeset + +import ( + "fmt" + + mcmslib "github.com/smartcontractkit/mcms" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" +) + +// SetFeedAdminChangeset is a changeset that sets/removes an admin on DataFeedsCache contract. +// This changeset may return a timelock proposal if the MCMS config is provided, otherwise it will execute the transaction with the deployer key. +var SetFeedAdminChangeset = deployment.CreateChangeSet(setFeedAdminLogic, setFeedAdminPrecondition) + +func setFeedAdminLogic(env deployment.Environment, c types.SetFeedAdminConfig) (deployment.ChangesetOutput, error) { + state, _ := LoadOnchainState(env) + chain := env.Chains[c.ChainSelector] + chainState := state.Chains[c.ChainSelector] + contract := chainState.DataFeedsCache[c.CacheAddress] + + txOpt := chain.DeployerKey + if c.McmsConfig != nil { + txOpt = deployment.SimTransactOpts() + } + + tx, err := contract.SetFeedAdmin(txOpt, c.AdminAddress, c.IsAdmin) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to set feed admin %w", err) + } + + if c.McmsConfig != nil { + proposal, err := BuildMCMProposals(env, "proposal to set feed admin on a cache", c.ChainSelector, []ProposalData{ + { + contract: contract.Address().Hex(), + tx: tx, + }, + }, c.McmsConfig.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil + } + _, err = chain.Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", tx.Hash().String(), err) + } + + return deployment.ChangesetOutput{}, nil +} + +func setFeedAdminPrecondition(env deployment.Environment, c types.SetFeedAdminConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + if c.McmsConfig != nil { + if err := ValidateMCMSAddresses(env.ExistingAddresses, c.ChainSelector); err != nil { + return err + } + } + + return ValidateCacheForChain(env, c.ChainSelector, c.CacheAddress) +} diff --git a/deployment/data-feeds/changeset/set_feed_admin_test.go b/deployment/data-feeds/changeset/set_feed_admin_test.go new file mode 100644 index 00000000000..320feffc81c --- /dev/null +++ b/deployment/data-feeds/changeset/set_feed_admin_test.go @@ -0,0 +1,98 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestSetCacheAdmin(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + newEnv, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.DeployMCMSWithTimelockV2), + map[uint64]commonTypes.MCMSWithTimelockConfigV2{ + chainSelector: proposalutils.SingleGroupTimelockConfigV2(t), + }, + ), + ) + require.NoError(t, err) + + cacheAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "DataFeedsCache") + require.NoError(t, err) + + // without MCMS + resp, err := commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress("0x123"), + IsAdmin: true, + }, + ), + ) + require.NoError(t, err) + require.NotNil(t, resp) + + // with MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.TransferToMCMSWithTimelockV2), + commonChangesets.TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chainSelector: {common.HexToAddress(cacheAddress)}, + }, + MinDelay: 0, + }, + ), + ) + require.NoError(t, err) + + resp, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress("0x123"), + IsAdmin: true, + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + ) + require.NoError(t, err) + require.NotNil(t, resp) +} diff --git a/deployment/data-feeds/changeset/set_feed_config.go b/deployment/data-feeds/changeset/set_feed_config.go new file mode 100644 index 00000000000..ea83d24d119 --- /dev/null +++ b/deployment/data-feeds/changeset/set_feed_config.go @@ -0,0 +1,73 @@ +package changeset + +import ( + "errors" + "fmt" + + mcmslib "github.com/smartcontractkit/mcms" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" +) + +// SetFeedConfigChangeset is a changeset that sets a feed configuration on DataFeedsCache contract. +// This changeset may return a timelock proposal if the MCMS config is provided, otherwise it will execute the transaction with the deployer key. +var SetFeedConfigChangeset = deployment.CreateChangeSet(setFeedConfigLogic, setFeedConfigPrecondition) + +func setFeedConfigLogic(env deployment.Environment, c types.SetFeedDecimalConfig) (deployment.ChangesetOutput, error) { + state, _ := LoadOnchainState(env) + chain := env.Chains[c.ChainSelector] + chainState := state.Chains[c.ChainSelector] + contract := chainState.DataFeedsCache[c.CacheAddress] + + txOpt := chain.DeployerKey + if c.McmsConfig != nil { + txOpt = deployment.SimTransactOpts() + } + + tx, err := contract.SetDecimalFeedConfigs(txOpt, c.DataIDs, c.Descriptions, c.WorkflowMetadata) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to set feed config %w", err) + } + + if c.McmsConfig != nil { + proposal, err := BuildMCMProposals(env, "proposal to set feed config on a cache", c.ChainSelector, []ProposalData{ + { + contract: contract.Address().Hex(), + tx: tx, + }, + }, c.McmsConfig.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil + } + _, err = chain.Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", tx.Hash().String(), err) + } + + return deployment.ChangesetOutput{}, nil +} + +func setFeedConfigPrecondition(env deployment.Environment, c types.SetFeedDecimalConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + if (len(c.DataIDs) == 0) || (len(c.Descriptions) == 0) || (len(c.WorkflowMetadata) == 0) { + return errors.New("dataIDs, descriptions and workflowMetadata must not be empty") + } + if len(c.DataIDs) != len(c.Descriptions) { + return errors.New("dataIDs and descriptions must have the same length") + } + + if c.McmsConfig != nil { + if err := ValidateMCMSAddresses(env.ExistingAddresses, c.ChainSelector); err != nil { + return err + } + } + + return ValidateCacheForChain(env, c.ChainSelector, c.CacheAddress) +} diff --git a/deployment/data-feeds/changeset/set_feed_config_test.go b/deployment/data-feeds/changeset/set_feed_config_test.go new file mode 100644 index 00000000000..fcc9b542d0e --- /dev/null +++ b/deployment/data-feeds/changeset/set_feed_config_test.go @@ -0,0 +1,138 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/shared" + cache "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/data_feeds_cache" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestSetFeedConfig(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + newEnv, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.DeployMCMSWithTimelockV2), + map[uint64]commonTypes.MCMSWithTimelockConfigV2{ + chainSelector: proposalutils.SingleGroupTimelockConfigV2(t), + }, + ), + ) + require.NoError(t, err) + + cacheAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "DataFeedsCache") + require.NoError(t, err) + + dataid, _ := shared.ConvertHexToBytes16("01bb0467f50003040000000000000000") + + // without MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(env.Chains[chainSelector].DeployerKey.From.Hex()), + IsAdmin: true, + }, + ), + commonChangesets.Configure( + changeset.SetFeedConfigChangeset, + types.SetFeedDecimalConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + DataIDs: [][16]byte{dataid}, + Descriptions: []string{"test"}, + WorkflowMetadata: []cache.DataFeedsCacheWorkflowMetadata{ + cache.DataFeedsCacheWorkflowMetadata{ + AllowedSender: common.HexToAddress("0x22"), + AllowedWorkflowOwner: common.HexToAddress("0x33"), + AllowedWorkflowName: shared.HashedWorkflowName("test"), + }, + }, + }, + ), + ) + require.NoError(t, err) + + // with MCMS + timeLockAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "RBACTimelock") + require.NoError(t, err) + + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // Set the admin to the timelock + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(timeLockAddress), + IsAdmin: true, + }, + ), + // Transfer cache ownership to MCMS + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.TransferToMCMSWithTimelockV2), + commonChangesets.TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chainSelector: {common.HexToAddress(cacheAddress)}, + }, + MinDelay: 0, + }, + ), + ) + require.NoError(t, err) + + // Set the feed config with MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.SetFeedConfigChangeset, + types.SetFeedDecimalConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + DataIDs: [][16]byte{dataid}, + Descriptions: []string{"test2"}, + WorkflowMetadata: []cache.DataFeedsCacheWorkflowMetadata{ + cache.DataFeedsCacheWorkflowMetadata{ + AllowedSender: common.HexToAddress("0x22"), + AllowedWorkflowOwner: common.HexToAddress("0x33"), + AllowedWorkflowName: shared.HashedWorkflowName("test"), + }, + }, + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + ) + require.NoError(t, err) +} diff --git a/deployment/data-feeds/changeset/state.go b/deployment/data-feeds/changeset/state.go new file mode 100644 index 00000000000..358c41cef9c --- /dev/null +++ b/deployment/data-feeds/changeset/state.go @@ -0,0 +1,143 @@ +package changeset + +import ( + "fmt" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/view" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/view/v1_0" + proxy "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/aggregator_proxy" + cache "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/data_feeds_cache" +) + +var ( + DataFeedsCache deployment.ContractType = "DataFeedsCache" +) + +type DataFeedsChainState struct { + commonchangeset.MCMSWithTimelockState + DataFeedsCache map[common.Address]*cache.DataFeedsCache + AggregatorProxy map[common.Address]*proxy.AggregatorProxy +} + +type DataFeedsOnChainState struct { + Chains map[uint64]DataFeedsChainState +} + +func LoadOnchainState(e deployment.Environment) (DataFeedsOnChainState, error) { + state := DataFeedsOnChainState{ + Chains: make(map[uint64]DataFeedsChainState), + } + for chainSelector, chain := range e.Chains { + addresses, err := e.ExistingAddresses.AddressesForChain(chainSelector) + if err != nil { + // Chain not found in address book, initialize empty + if !errors.Is(err, deployment.ErrChainNotFound) { + return state, err + } + addresses = make(map[string]deployment.TypeAndVersion) + } + chainState, err := LoadChainState(e.Logger, chain, addresses) + if err != nil { + return state, err + } + state.Chains[chainSelector] = *chainState + } + return state, nil +} + +// LoadChainState Loads all state for a chain into state +func LoadChainState(logger logger.Logger, chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (*DataFeedsChainState, error) { + var state DataFeedsChainState + + mcmsWithTimelock, err := commonchangeset.MaybeLoadMCMSWithTimelockChainState(chain, addresses) + if err != nil { + return nil, fmt.Errorf("failed to load mcms contract: %w", err) + } + state.MCMSWithTimelockState = *mcmsWithTimelock + + dfCacheTV := deployment.NewTypeAndVersion(DataFeedsCache, deployment.Version1_0_0) + dfCacheTV.Labels.Add("data-feeds") + + devPlatformCacheTV := deployment.NewTypeAndVersion(DataFeedsCache, deployment.Version1_0_0) + devPlatformCacheTV.Labels.Add("dev-platform") + + state.DataFeedsCache = make(map[common.Address]*cache.DataFeedsCache) + state.AggregatorProxy = make(map[common.Address]*proxy.AggregatorProxy) + + for address, tv := range addresses { + switch { + case tv.String() == dfCacheTV.String() || tv.String() == devPlatformCacheTV.String(): + contract, err := cache.NewDataFeedsCache(common.HexToAddress(address), chain.Client) + if err != nil { + return &state, err + } + state.DataFeedsCache[common.HexToAddress(address)] = contract + case strings.Contains(tv.String(), "AggregatorProxy"): + contract, err := proxy.NewAggregatorProxy(common.HexToAddress(address), chain.Client) + if err != nil { + return &state, err + } + state.AggregatorProxy[common.HexToAddress(address)] = contract + default: + logger.Warnw("unknown contract type", "type", tv.Type) + } + } + return &state, nil +} + +func (s DataFeedsOnChainState) View(chains []uint64) (map[string]view.ChainView, error) { + m := make(map[string]view.ChainView) + for _, chainSelector := range chains { + chainInfo, err := deployment.ChainInfo(chainSelector) + if err != nil { + return m, err + } + if _, ok := s.Chains[chainSelector]; !ok { + return m, fmt.Errorf("chain not supported %d", chainSelector) + } + chainState := s.Chains[chainSelector] + chainView, err := chainState.GenerateView() + if err != nil { + return m, err + } + name := chainInfo.ChainName + if chainInfo.ChainName == "" { + name = strconv.FormatUint(chainSelector, 10) + } + m[name] = chainView + } + return m, nil +} + +func (c DataFeedsChainState) GenerateView() (view.ChainView, error) { + chainView := view.NewChain() + if c.DataFeedsCache != nil { + for _, cache := range c.DataFeedsCache { + fmt.Println(cache.Address().Hex()) + cacheView, err := v1_0.GenerateDataFeedsCacheView(cache) + if err != nil { + return chainView, errors.Wrapf(err, "failed to generate cache view %s", cache.Address().String()) + } + chainView.DataFeedsCache[cache.Address().Hex()] = cacheView + } + } + if c.AggregatorProxy != nil { + for _, proxy := range c.AggregatorProxy { + proxyView, err := v1_0.GenerateAggregatorProxyView(proxy) + if err != nil { + return chainView, errors.Wrapf(err, "failed to generate proxy view %s", proxy.Address().String()) + } + chainView.AggregatorProxy[proxy.Address().Hex()] = proxyView + } + } + return chainView, nil +} diff --git a/deployment/data-feeds/changeset/testdata/import_addresses.json b/deployment/data-feeds/changeset/testdata/import_addresses.json new file mode 100644 index 00000000000..5dafd6b24bd --- /dev/null +++ b/deployment/data-feeds/changeset/testdata/import_addresses.json @@ -0,0 +1,18 @@ +[ + { + "address": "0x33442400910b7B03316fe47eF8fC7bEd54Bca407", + "description": "TEST / USD", + "typeAndVersion": { + "type": "AggregatorProxy", + "version": "1.0.0" + } + }, + { + "address": "0x43442400910b7B03316fe47eF8fC7bEd54Bca407", + "description": "LINK / USD", + "typeAndVersion": { + "type": "AggregatorProxy", + "version": "1.0.0" + } + } +] \ No newline at end of file diff --git a/deployment/data-feeds/changeset/testdata/migrate_feeds.json b/deployment/data-feeds/changeset/testdata/migrate_feeds.json new file mode 100644 index 00000000000..83fca1cb0b1 --- /dev/null +++ b/deployment/data-feeds/changeset/testdata/migrate_feeds.json @@ -0,0 +1,20 @@ +[ + { + "address": "0x33442400910b7B03316fe47eF8fC7bEd54Bca407", + "feedId": "01bb0467f50003040000000000000000", + "description": "TEST / USD", + "typeAndVersion": { + "type": "AggregatorProxy", + "version": "1.0.0" + } + }, + { + "address": "0x43442400910b7B03316fe47eF8fC7bEd54Bca407", + "feedId": "01b40467f50003040000000000000000", + "description": "LINK / USD", + "typeAndVersion": { + "type": "AggregatorProxy", + "version": "1.0.0" + } + } +] \ No newline at end of file diff --git a/deployment/data-feeds/changeset/types/types.go b/deployment/data-feeds/changeset/types/types.go new file mode 100644 index 00000000000..ff5b396f6c5 --- /dev/null +++ b/deployment/data-feeds/changeset/types/types.go @@ -0,0 +1,133 @@ +package types + +import ( + "embed" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/deployment" + proxy "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/aggregator_proxy" + cache "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/data_feeds_cache" +) + +type MCMSConfig struct { + MinDelay time.Duration // delay for timelock worker to execute the transfers. +} + +type AddressType string + +type DeployCacheResponse struct { + Address common.Address + Tx common.Hash + Tv deployment.TypeAndVersion + Contract *cache.DataFeedsCache +} + +type DeployConfig struct { + ChainsToDeploy []uint64 // Chain Selectors + Labels []string // Labels for the cache, applies to all chains +} + +type DeployAggregatorProxyConfig struct { + ChainsToDeploy []uint64 // Chain Selectors + AccessController []common.Address // AccessController addresses per chain + Labels []string // Labels for the cache, applies to all chains +} + +type DeployBundleAggregatorProxyConfig struct { + ChainsToDeploy []uint64 // Chain Selectors + MCMSAddressesPath string // Path to the MCMS addresses JSON file, per chain + InputFS embed.FS // Filesystem to read MCMS addresses JSON file +} + +type DeployProxyResponse struct { + Address common.Address + Tx common.Hash + Tv deployment.TypeAndVersion + Contract *proxy.AggregatorProxy +} + +type SetFeedAdminConfig struct { + ChainSelector uint64 + CacheAddress common.Address + AdminAddress common.Address + IsAdmin bool + McmsConfig *MCMSConfig +} + +type ProposeConfirmAggregatorConfig struct { + ChainSelector uint64 + ProxyAddress common.Address + NewAggregatorAddress common.Address + McmsConfig *MCMSConfig +} + +type SetFeedDecimalConfig struct { + ChainSelector uint64 + CacheAddress common.Address + DataIDs [][16]byte // without the 0x prefix + Descriptions []string + WorkflowMetadata []cache.DataFeedsCacheWorkflowMetadata + McmsConfig *MCMSConfig +} + +type RemoveFeedConfig struct { + ChainSelector uint64 + CacheAddress common.Address + ProxyAddresses []common.Address + DataIDs [][16]byte // without the 0x prefix + McmsConfig *MCMSConfig +} + +type RemoveFeedConfigCSConfig struct { + ChainSelector uint64 + CacheAddress common.Address + DataIDs [][16]byte // without the 0x prefix + McmsConfig *MCMSConfig +} + +type UpdateDataIDProxyConfig struct { + ChainSelector uint64 + CacheAddress common.Address + ProxyAddresses []common.Address + DataIDs [][16]byte + McmsConfig *MCMSConfig +} + +type RemoveFeedProxyConfig struct { + ChainSelector uint64 + CacheAddress common.Address + ProxyAddresses []common.Address + McmsConfig *MCMSConfig +} + +type ImportToAddressbookConfig struct { + InputFileName string + ChainSelector uint64 + InputFS embed.FS +} + +type MigrationConfig struct { + InputFileName string + CacheAddress common.Address + ChainSelector uint64 + InputFS embed.FS + WorkflowMetadata []cache.DataFeedsCacheWorkflowMetadata +} + +type AcceptOwnershipConfig struct { + ContractAddress common.Address + ChainSelector uint64 + McmsConfig *MCMSConfig +} + +type NewFeedWithProxyConfig struct { + ChainSelector uint64 + AccessController common.Address + Labels []string // labels for AggregatorProxy + DataID [16]byte // without the 0x prefix + Description string + WorkflowMetadata []cache.DataFeedsCacheWorkflowMetadata + McmsConfig *MCMSConfig +} diff --git a/deployment/data-feeds/changeset/update_data_id_proxy.go b/deployment/data-feeds/changeset/update_data_id_proxy.go new file mode 100644 index 00000000000..95115e3ca06 --- /dev/null +++ b/deployment/data-feeds/changeset/update_data_id_proxy.go @@ -0,0 +1,73 @@ +package changeset + +import ( + "errors" + "fmt" + + mcmslib "github.com/smartcontractkit/mcms" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" +) + +// UpdateDataIDProxyChangeset is a changeset that updates the proxy-dataId mapping on DataFeedsCache contract. +// This changeset may return a timelock proposal if the MCMS config is provided, otherwise it will execute the transaction with the deployer key. +var UpdateDataIDProxyChangeset = deployment.CreateChangeSet(updateDataIDProxyLogic, updateDataIDProxyPrecondition) + +func updateDataIDProxyLogic(env deployment.Environment, c types.UpdateDataIDProxyConfig) (deployment.ChangesetOutput, error) { + state, _ := LoadOnchainState(env) + chain := env.Chains[c.ChainSelector] + chainState := state.Chains[c.ChainSelector] + contract := chainState.DataFeedsCache[c.CacheAddress] + + txOpt := chain.DeployerKey + if c.McmsConfig != nil { + txOpt = deployment.SimTransactOpts() + } + + tx, err := contract.UpdateDataIdMappingsForProxies(txOpt, c.ProxyAddresses, c.DataIDs) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to set proxy-dataId mapping %w", err) + } + + if c.McmsConfig != nil { + proposal, err := BuildMCMProposals(env, "proposal to update proxy-dataId mapping on a cache", c.ChainSelector, []ProposalData{ + { + contract: contract.Address().Hex(), + tx: tx, + }, + }, c.McmsConfig.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal: %w", err) + } + return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil + } + _, err = chain.Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to confirm transaction: %s, %w", tx.Hash().String(), err) + } + + return deployment.ChangesetOutput{}, nil +} + +func updateDataIDProxyPrecondition(env deployment.Environment, c types.UpdateDataIDProxyConfig) error { + _, ok := env.Chains[c.ChainSelector] + if !ok { + return fmt.Errorf("chain not found in env %d", c.ChainSelector) + } + + if len(c.ProxyAddresses) == 0 || len(c.DataIDs) == 0 { + return errors.New("empty proxies or dataIds") + } + if len(c.DataIDs) != len(c.ProxyAddresses) { + return errors.New("dataIds and proxies length mismatch") + } + + if c.McmsConfig != nil { + if err := ValidateMCMSAddresses(env.ExistingAddresses, c.ChainSelector); err != nil { + return err + } + } + + return ValidateCacheForChain(env, c.ChainSelector, c.CacheAddress) +} diff --git a/deployment/data-feeds/changeset/update_data_id_proxy_test.go b/deployment/data-feeds/changeset/update_data_id_proxy_test.go new file mode 100644 index 00000000000..779db3a37be --- /dev/null +++ b/deployment/data-feeds/changeset/update_data_id_proxy_test.go @@ -0,0 +1,122 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + commonChangesets "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/types" + "github.com/smartcontractkit/chainlink/deployment/data-feeds/shared" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" +) + +func TestUpdateDataIDProxyMap(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + cfg := memory.MemoryEnvironmentConfig{ + Nodes: 1, + Chains: 1, + } + env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) + + chainSelector := env.AllChainSelectors()[0] + + newEnv, err := commonChangesets.Apply(t, env, nil, + commonChangesets.Configure( + changeset.DeployCacheChangeset, + types.DeployConfig{ + ChainsToDeploy: []uint64{chainSelector}, + Labels: []string{"data-feeds"}, + }, + ), + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.DeployMCMSWithTimelockV2), + map[uint64]commonTypes.MCMSWithTimelockConfigV2{ + chainSelector: proposalutils.SingleGroupTimelockConfigV2(t), + }, + ), + ) + require.NoError(t, err) + + cacheAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "DataFeedsCache") + require.NoError(t, err) + + dataID, _ := shared.ConvertHexToBytes16("01bb0467f50003040000000000000000") + + // without MCMS + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(env.Chains[chainSelector].DeployerKey.From.Hex()), + IsAdmin: true, + }, + ), + commonChangesets.Configure( + changeset.UpdateDataIDProxyChangeset, + types.UpdateDataIDProxyConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + ProxyAddresses: []common.Address{common.HexToAddress("0x11")}, + DataIDs: [][16]byte{dataID}, + }, + ), + ) + require.NoError(t, err) + + // with MCMS + timeLockAddress, err := deployment.SearchAddressBook(newEnv.ExistingAddresses, chainSelector, "RBACTimelock") + require.NoError(t, err) + + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + // Set the admin to the timelock + commonChangesets.Configure( + changeset.SetFeedAdminChangeset, + types.SetFeedAdminConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + AdminAddress: common.HexToAddress(timeLockAddress), + IsAdmin: true, + }, + ), + // Transfer cache ownership to MCMS + commonChangesets.Configure( + deployment.CreateLegacyChangeSet(commonChangesets.TransferToMCMSWithTimelockV2), + commonChangesets.TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chainSelector: {common.HexToAddress(cacheAddress)}, + }, + MinDelay: 0, + }, + ), + ) + require.NoError(t, err) + + newEnv, err = commonChangesets.Apply(t, newEnv, nil, + commonChangesets.Configure( + changeset.UpdateDataIDProxyChangeset, + types.UpdateDataIDProxyConfig{ + ChainSelector: chainSelector, + CacheAddress: common.HexToAddress(cacheAddress), + ProxyAddresses: []common.Address{common.HexToAddress("0x11")}, + DataIDs: [][16]byte{dataID}, + McmsConfig: &types.MCMSConfig{ + MinDelay: 0, + }, + }, + ), + ) + require.NoError(t, err) +} diff --git a/deployment/data-feeds/changeset/validation.go b/deployment/data-feeds/changeset/validation.go new file mode 100644 index 00000000000..ac9c7a758cd --- /dev/null +++ b/deployment/data-feeds/changeset/validation.go @@ -0,0 +1,45 @@ +package changeset + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + + commonTypes "github.com/smartcontractkit/chainlink/deployment/common/types" + + "github.com/smartcontractkit/chainlink/deployment" +) + +func ValidateCacheForChain(env deployment.Environment, chainSelector uint64, cacheAddress common.Address) error { + state, err := LoadOnchainState(env) + if err != nil { + return fmt.Errorf("failed to load on chain state %w", err) + } + _, ok := env.Chains[chainSelector] + if !ok { + return errors.New("chain not found in environment") + } + chainState, ok := state.Chains[chainSelector] + if !ok { + return errors.New("chain not found in on chain state") + } + if chainState.DataFeedsCache == nil { + return errors.New("DataFeedsCache not found in on chain state") + } + _, ok = chainState.DataFeedsCache[cacheAddress] + if !ok { + return errors.New("contract not found in on chain state") + } + return nil +} + +func ValidateMCMSAddresses(ab deployment.AddressBook, chainSelector uint64) error { + if _, err := deployment.SearchAddressBook(ab, chainSelector, commonTypes.RBACTimelock); err != nil { + return fmt.Errorf("timelock not present on the chain %w", err) + } + if _, err := deployment.SearchAddressBook(ab, chainSelector, commonTypes.ProposerManyChainMultisig); err != nil { + return fmt.Errorf("mcms proposer not present on the chain %w", err) + } + return nil +} diff --git a/deployment/data-feeds/changeset/view.go b/deployment/data-feeds/changeset/view.go new file mode 100644 index 00000000000..15348e1f8e1 --- /dev/null +++ b/deployment/data-feeds/changeset/view.go @@ -0,0 +1,26 @@ +package changeset + +import ( + "encoding/json" + "fmt" + + "github.com/smartcontractkit/chainlink/deployment" + dfView "github.com/smartcontractkit/chainlink/deployment/data-feeds/view" +) + +var _ deployment.ViewState = ViewDataFeeds + +func ViewDataFeeds(e deployment.Environment) (json.Marshaler, error) { + state, err := LoadOnchainState(e) + fmt.Println(state) + if err != nil { + return nil, err + } + chainView, err := state.View(e.AllChainSelectors()) + if err != nil { + return nil, err + } + return dfView.DataFeedsView{ + Chains: chainView, + }, nil +} diff --git a/deployment/data-feeds/shared/utils.go b/deployment/data-feeds/shared/utils.go new file mode 100644 index 00000000000..b3549065f3f --- /dev/null +++ b/deployment/data-feeds/shared/utils.go @@ -0,0 +1,50 @@ +package shared + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io/fs" +) + +func LoadJSON[T any](pth string, fs fs.ReadFileFS) (T, error) { + var dflt T + f, err := fs.ReadFile(pth) + if err != nil { + return dflt, fmt.Errorf("failed to read %s: %w", pth, err) + } + var v T + err = json.Unmarshal(f, &v) + if err != nil { + return dflt, fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return v, nil +} + +func ConvertHexToBytes16(hexStr string) ([16]byte, error) { + decodedBytes, err := hex.DecodeString(hexStr) + if err != nil { + return [16]byte{}, fmt.Errorf("failed to decode hex string: %w", err) + } + + var result [16]byte + copy(result[:], decodedBytes[:16]) + + return result, nil +} + +func HashedWorkflowName(name string) [10]byte { + // Compute SHA-256 hash of the input string + hash := sha256.Sum256([]byte(name)) + + // Encode as hex to ensure UTF8 + var hashBytes = hash[:] + resultHex := hex.EncodeToString(hashBytes) + + // Truncate to 10 bytes + var truncated [10]byte + copy(truncated[:], []byte(resultHex)[:10]) + + return truncated +} diff --git a/deployment/data-feeds/view/v1_0/cache_contract.go b/deployment/data-feeds/view/v1_0/cache_contract.go new file mode 100644 index 00000000000..5fd06c046c4 --- /dev/null +++ b/deployment/data-feeds/view/v1_0/cache_contract.go @@ -0,0 +1,28 @@ +package v1_0 + +import ( + "errors" + "fmt" + + "github.com/smartcontractkit/chainlink/deployment/common/view/types" + cache "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/data_feeds_cache" +) + +type CacheView struct { + types.ContractMetaData +} + +// GenerateDataFeedsCacheView generates a CacheView from a DataFeedsCache contract. +func GenerateDataFeedsCacheView(cache *cache.DataFeedsCache) (CacheView, error) { + if cache == nil { + return CacheView{}, errors.New("cannot generate view for nil DataFeedsCache") + } + meta, err := types.NewContractMetaData(cache, cache.Address()) + if err != nil { + return CacheView{}, fmt.Errorf("failed to generate contract metadata for DataFeedsCache: %w", err) + } + + return CacheView{ + ContractMetaData: meta, + }, nil +} diff --git a/deployment/data-feeds/view/v1_0/proxy_contract.go b/deployment/data-feeds/view/v1_0/proxy_contract.go new file mode 100644 index 00000000000..96d4cb25f79 --- /dev/null +++ b/deployment/data-feeds/view/v1_0/proxy_contract.go @@ -0,0 +1,48 @@ +package v1_0 + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + + proxy "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/data-feeds/generated/aggregator_proxy" +) + +type ProxyView struct { + TypeAndVersion string `json:"typeAndVersion,omitempty"` + Address common.Address `json:"address,omitempty"` + Owner common.Address `json:"owner,omitempty"` + Description string `json:"description,omitempty"` + Aggregator common.Address `json:"aggregator,omitempty"` +} + +// GenerateAggregatorProxyView generates a ProxyView from a AggregatorProxy contract. +func GenerateAggregatorProxyView(proxy *proxy.AggregatorProxy) (ProxyView, error) { + if proxy == nil { + return ProxyView{}, errors.New("cannot generate view for nil AggregatorProxy") + } + + description, err := proxy.Description(nil) + if err != nil { + return ProxyView{}, fmt.Errorf("failed to get description for AggregatorProxy: %w", err) + } + + owner, err := proxy.Owner(nil) + if err != nil { + return ProxyView{}, fmt.Errorf("failed to get owner for AggregatorProxy: %w", err) + } + + aggregator, err := proxy.Aggregator(nil) + if err != nil { + return ProxyView{}, fmt.Errorf("failed to get aggregator for AggregatorProxy: %w", err) + } + + return ProxyView{ + Address: proxy.Address(), + Owner: owner, + Description: description, + TypeAndVersion: "AggregatorProxy 1.0.0", + Aggregator: aggregator, + }, nil +} diff --git a/deployment/data-feeds/view/view.go b/deployment/data-feeds/view/view.go new file mode 100644 index 00000000000..ef5ae9926c9 --- /dev/null +++ b/deployment/data-feeds/view/view.go @@ -0,0 +1,31 @@ +package view + +import ( + "encoding/json" + + "github.com/smartcontractkit/chainlink/deployment/data-feeds/view/v1_0" +) + +type ChainView struct { + // v1.0 + DataFeedsCache map[string]v1_0.CacheView `json:"dataFeedsCache,omitempty"` + AggregatorProxy map[string]v1_0.ProxyView `json:"aggregatorProxy,omitempty"` +} + +func NewChain() ChainView { + return ChainView{ + // v1.0 + DataFeedsCache: make(map[string]v1_0.CacheView), + AggregatorProxy: make(map[string]v1_0.ProxyView), + } +} + +type DataFeedsView struct { + Chains map[string]ChainView `json:"chains,omitempty"` +} + +func (v DataFeedsView) MarshalJSON() ([]byte, error) { + // Alias to avoid recursive calls + type Alias DataFeedsView + return json.MarshalIndent(&struct{ Alias }{Alias: Alias(v)}, "", " ") +}