diff --git a/deployment/cre/contracts/contracts.go b/deployment/cre/contracts/contracts.go index cf885a56971..4f8c3a273ed 100644 --- a/deployment/cre/contracts/contracts.go +++ b/deployment/cre/contracts/contracts.go @@ -35,6 +35,7 @@ var ( RBACTimelock cldf.ContractType = "RBACTimelock" // no type and a version in contract https://github.com/smartcontractkit/ccip-owner-contracts/blob/main/src/RBACTimelock.sol ProposerManyChainMultiSig cldf.ContractType = "ProposerManyChainMultiSig" // no type and a version in contract https://github.com/smartcontractkit/ccip-owner-contracts/blob/main/src/ManyChainMultiSig.sol ShardConfig cldf.ContractType = "ShardConfig" // manages desired shard count configuration + MockKeystoneForwarder cldf.ContractType = "MockKeystoneForwarder" // https://github.com/smartcontractkit/chainlink-evm/blob/f2272e4b4aa6a3e315126ce7d928472bb035f940/contracts/cre/src/dev/MockKeystoneForwarder.sol#L38 ) type MCMSConfig = proposalutils.TimelockConfig diff --git a/deployment/cre/forwarder/deploy_mock.go b/deployment/cre/forwarder/deploy_mock.go new file mode 100644 index 00000000000..7e77abc0256 --- /dev/null +++ b/deployment/cre/forwarder/deploy_mock.go @@ -0,0 +1,190 @@ +package forwarder + +import ( + "context" + "errors" + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "golang.org/x/sync/errgroup" + + chain_selectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + mock_forwarder "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/mock_forwarder" +) + +var _ cldf.ChangeSetV2[DeployMockForwardersInput] = DeployMockForwarders{} + +// DeployMockForwardersInput is the input for deploying MockKeystoneForwarder contracts. +type DeployMockForwardersInput struct { + Targets []uint64 `json:"targets" yaml:"targets"` + Qualifier string `json:"qualifier" yaml:"qualifier"` +} + +// DeployMockForwarders is a ChangeSetV2 that deploys MockKeystoneForwarder contracts. +type DeployMockForwarders struct{} + +func (d DeployMockForwarders) VerifyPreconditions(env cldf.Environment, input DeployMockForwardersInput) error { + if input.Qualifier == "" { + return errors.New("qualifier is required") + } + for _, sel := range input.Targets { + if _, err := chain_selectors.GetChainIDFromSelector(sel); err != nil { + return fmt.Errorf("could not resolve chain selector %d: %w", sel, err) + } + if _, ok := env.BlockChains.EVMChains()[sel]; !ok { + return fmt.Errorf("chain selector %d not found in environment", sel) + } + } + return nil +} + +func (d DeployMockForwarders) Apply(env cldf.Environment, input DeployMockForwardersInput) (cldf.ChangesetOutput, error) { + seqReport, err := operations.ExecuteSequence( + env.OperationsBundle, + DeployMockSequence, + DeploySequenceDeps{Env: &env}, + DeploySequenceInput(input), + ) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to execute deploy mock forwarders sequence: %w", err) + } + + ds := datastore.NewMemoryDataStore() + addrs, err := seqReport.Output.Addresses.Fetch() + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to fetch addresses from sequence output: %w", err) + } + for _, addr := range addrs { + if err := ds.Addresses().Add(addr); err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to add address ref to mutable datastore: %w", err) + } + } + + return cldf.ChangesetOutput{ + DataStore: ds, + Reports: seqReport.ExecutionReports, + }, nil +} + +type DeployMockForwarderSequenceOutput struct { + Addresses datastore.AddressRefStore + Datastore datastore.DataStore +} + +// DeployMockSequence deploys MockKeystoneForwarder contracts to multiple chains concurrently. +var DeployMockSequence = operations.NewSequence( + "deploy-mock-keystone-forwarders-seq", + semver.MustParse("1.0.0"), + "Deploy Mock Keystone Forwarders", + func(b operations.Bundle, deps DeploySequenceDeps, input DeploySequenceInput) (DeployMockForwarderSequenceOutput, error) { + as := datastore.NewMemoryDataStore() + contractErrGroup := &errgroup.Group{} + for _, target := range input.Targets { + contractErrGroup.Go(func() error { + r, err := operations.ExecuteOperation(b, DeployMockOp, DeployOpDeps(deps), DeployOpInput{ + ChainSelector: target, + Qualifier: input.Qualifier, + }) + if err != nil { + return err + } + addrs, err := r.Output.Addresses.Fetch() + if err != nil { + return fmt.Errorf("failed to fetch MockKeystoneForwarder addresses for target %d: %w", target, err) + } + for _, addr := range addrs { + if addrRefErr := as.AddressRefStore.Add(addr); addrRefErr != nil { + return fmt.Errorf("failed to save MockKeystoneForwarder address on datastore for target %d: %w", target, addrRefErr) + } + } + + return nil + }) + } + if err := contractErrGroup.Wait(); err != nil { + return DeployMockForwarderSequenceOutput{Addresses: as.Addresses()}, fmt.Errorf("failed to deploy MockKeystoneForwarder contracts: %w", err) + } + return DeployMockForwarderSequenceOutput{Addresses: as.Addresses(), Datastore: as.Seal()}, nil + }, +) + +type DeployMockForwarderOpOutput struct { + Addresses datastore.AddressRefStore + AddressRef datastore.AddressRef // The address ref of the deployed Keystone Forwarder +} + +// DeployMockOp is an operation that deploys the MockKeystoneForwarder contract. +var DeployMockOp = operations.NewOperation( + "deploy-mock-keystone-forwarder-op", + semver.MustParse("1.0.0"), + "Deploy MockKeystoneForwarder Contract", + func(b operations.Bundle, deps DeployOpDeps, input DeployOpInput) (DeployMockForwarderOpOutput, error) { + chain, ok := deps.Env.BlockChains.EVMChains()[input.ChainSelector] + if !ok { + return DeployMockForwarderOpOutput{}, fmt.Errorf("deploy-mock-keystone-forwarder-op failed: chain selector %d not found in environment", input.ChainSelector) + } + addr, tv, err := deployMock(b.GetContext(), chain.DeployerKey, chain) + if err != nil { + return DeployMockForwarderOpOutput{}, fmt.Errorf("deploy-mock-keystone-forwarder-op failed: %w", err) + } + labels := tv.Labels.List() + labels = append(labels, input.Labels...) + r := datastore.AddressRef{ + ChainSelector: input.ChainSelector, + Address: addr.String(), + Type: datastore.ContractType(tv.Type), + Version: &tv.Version, + Qualifier: input.Qualifier, + Labels: datastore.NewLabelSet(labels...), + } + ds := datastore.NewMemoryDataStore() + if err := ds.AddressRefStore.Add(r); err != nil { + return DeployMockForwarderOpOutput{}, fmt.Errorf("deploy-mock-keystone-forwarder-op failed: failed to add address ref to datastore: %w", err) + } + + return DeployMockForwarderOpOutput{ + Addresses: ds.Addresses(), + AddressRef: r, + }, nil + }, +) + +func deployMock(ctx context.Context, auth *bind.TransactOpts, chain evm.Chain) (*common.Address, *cldf.TypeAndVersion, error) { + forwarderAddr, tx, mockForwarderContract, err := mock_forwarder.DeployMockKeystoneForwarder( + auth, + chain.Client) + if err != nil { + return nil, nil, fmt.Errorf("failed to deploy MockKeystoneForwarder: %w", err) + } + + _, err = chain.Confirm(tx) + if err != nil { + return nil, nil, fmt.Errorf("failed to confirm and save MockKeystoneForwarder: %w", err) + } + tvStr, err := mockForwarderContract.TypeAndVersion(&bind.CallOpts{}) + if err != nil { + return nil, nil, fmt.Errorf("failed to get type and version: %w", err) + } + tv, err := cldf.TypeAndVersionFromString(tvStr) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse type and version from %s: %w", tvStr, err) + } + txHash := tx.Hash() + txReceipt, err := chain.Client.TransactionReceipt(ctx, tx.Hash()) + if err != nil { + return nil, nil, fmt.Errorf("failed to get transaction receipt: %w", err) + } + hashLabel := fmt.Sprintf("%s: %s", DeploymentHashLabel, txHash.Hex()) + blockLabel := fmt.Sprintf("%s: %s", DeploymentBlockLabel, txReceipt.BlockNumber.String()) + tv.Labels.Add(blockLabel) + tv.Labels.Add(hashLabel) + + return &forwarderAddr, &tv, nil +} diff --git a/deployment/cre/forwarder/deploy_mock_test.go b/deployment/cre/forwarder/deploy_mock_test.go new file mode 100644 index 00000000000..9fc130ea3a9 --- /dev/null +++ b/deployment/cre/forwarder/deploy_mock_test.go @@ -0,0 +1,51 @@ +package forwarder_test + +import ( + "testing" + + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" + + "github.com/smartcontractkit/chainlink/deployment/cre/contracts" + "github.com/smartcontractkit/chainlink/deployment/cre/forwarder" +) + +func TestDeployMockForwarder(t *testing.T) { + t.Parallel() + + registrySel := chainsel.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{registrySel}), + )) + require.NoError(t, err) + + err = rt.Exec( + runtime.ChangesetTask(forwarder.DeployMockForwarders{}, + forwarder.DeployMockForwardersInput{ + Targets: []uint64{registrySel}, + Qualifier: "my-test-mock-forwarder", + }, + ), + ) + require.NoError(t, err) + + addrs := rt.State().DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(registrySel), + ) + require.Len(t, addrs, 1) + + mockAddrs := rt.State().DataStore.Addresses().Filter( + datastore.AddressRefByType(datastore.ContractType(contracts.MockKeystoneForwarder)), + ) + require.Len(t, mockAddrs, 1) + require.Equal(t, "my-test-mock-forwarder", mockAddrs[0].Qualifier) + + labels := mockAddrs[0].Labels.List() + require.Len(t, labels, 2) + require.Contains(t, labels[0], forwarder.DeploymentBlockLabel) + require.Contains(t, labels[1], forwarder.DeploymentHashLabel) +}