From d76dcea4816ad8506c7c664f01fb70354ef65bf2 Mon Sep 17 00:00:00 2001 From: "Simon B.Robert" Date: Fri, 20 Dec 2024 08:57:09 -0500 Subject: [PATCH] Initial commit --- .../ccip/changeset/cs_rmn_curse_uncurse.go | 138 ++++++++++++++++++ .../changeset/cs_rmn_curse_uncurse_test.go | 61 ++++++++ .../ccip/changeset/cs_update_rmn_config.go | 29 ---- deployment/ccip/changeset/deployer_group.go | 131 +++++++++++++++++ 4 files changed, 330 insertions(+), 29 deletions(-) create mode 100644 deployment/ccip/changeset/cs_rmn_curse_uncurse.go create mode 100644 deployment/ccip/changeset/cs_rmn_curse_uncurse_test.go create mode 100644 deployment/ccip/changeset/deployer_group.go diff --git a/deployment/ccip/changeset/cs_rmn_curse_uncurse.go b/deployment/ccip/changeset/cs_rmn_curse_uncurse.go new file mode 100644 index 00000000000..0e2eaa8b843 --- /dev/null +++ b/deployment/ccip/changeset/cs_rmn_curse_uncurse.go @@ -0,0 +1,138 @@ +package changeset + +import ( + "encoding/binary" + "fmt" + + "github.com/smartcontractkit/chainlink/deployment" +) + +const ( + GLOBAL_CURSE_SUBJECT = 0 +) + +type RMNCurseAction struct { + ChainSelector uint64 + SubjectToCurse uint64 +} + +type CurseAction func(e deployment.Environment) []RMNCurseAction + +type RMNCurseConfig struct { + HomeChainSelector uint64 + MCMS *MCMSConfig + CurseActions []CurseAction + CurseReason string +} + +func subjectToByte16(subject uint64) [16]byte { + var b [16]byte + binary.LittleEndian.PutUint64(b[:8], subject) + return b +} + +func CurseLane(sourceSelector uint64, destinationSelector uint64) CurseAction { + // Bidirectional curse between two chains + return func(e deployment.Environment) []RMNCurseAction { + return []RMNCurseAction{ + { + ChainSelector: sourceSelector, + SubjectToCurse: destinationSelector, + }, + { + ChainSelector: destinationSelector, + SubjectToCurse: sourceSelector, + }, + } + } +} + +func CurseChain(chainSelector uint64) CurseAction { + return func(e deployment.Environment) []RMNCurseAction { + chainSelectors := e.AllChainSelectors() + + // Curse all other chains to prevent onramp from sending message to the cursed chain + var curseActions []RMNCurseAction + for _, otherChainSelector := range chainSelectors { + if otherChainSelector != chainSelector { + curseActions = append(curseActions, RMNCurseAction{ + ChainSelector: otherChainSelector, + SubjectToCurse: chainSelector, + }) + } + } + + // Curse the chain with a global curse to prevent any onramp or offramp message from send message in and out of the chain + curseActions = append(curseActions, RMNCurseAction{ + ChainSelector: chainSelector, + SubjectToCurse: GLOBAL_CURSE_SUBJECT, + }) + + return curseActions + } +} + +func groupRMNSubjectBySelector(rmnSubjects []RMNCurseAction) map[uint64][]uint64 { + grouped := make(map[uint64][]uint64) + for _, subject := range rmnSubjects { + grouped[subject.ChainSelector] = append(grouped[subject.ChainSelector], subject.SubjectToCurse) + } + + // Only keep unique subjects, preserve only global curse if present and eliminate any curse where the selector is the same as the subject + for chainSelector, subjects := range grouped { + uniqueSubjects := make(map[uint64]struct{}) + for _, subject := range subjects { + if subject == chainSelector { + continue + } + uniqueSubjects[subject] = struct{}{} + } + + if _, ok := uniqueSubjects[GLOBAL_CURSE_SUBJECT]; ok { + grouped[chainSelector] = []uint64{GLOBAL_CURSE_SUBJECT} + } else { + var uniqueSubjectsSlice []uint64 + for subject := range uniqueSubjects { + uniqueSubjectsSlice = append(uniqueSubjectsSlice, subject) + } + grouped[chainSelector] = uniqueSubjectsSlice + } + } + + return grouped +} + +func NewRMNCurseChangeset(e deployment.Environment, cfg RMNCurseConfig) (deployment.ChangesetOutput, error) { + state, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load onchain state: %w", err) + } + deployerGroup := NewDeployerGroup(e, state, cfg.MCMS) + + // Generate curse actions + var curseActions []RMNCurseAction + for _, curseAction := range cfg.CurseActions { + curseActions = append(curseActions, curseAction(e)...) + } + + // Group curse actions by chain selector + grouped := groupRMNSubjectBySelector(curseActions) + + // For each chain in the environement get the RMNRemote contract and call curse + for selector, chain := range state.Chains { + deployer := deployerGroup.getDeployer(selector) + if curseSubjects, ok := grouped[selector]; ok { + subjectsByte16 := make([][16]byte, len(curseSubjects)) + for i, subject := range curseSubjects { + subjectsByte16[i] = subjectToByte16(subject) + } + + _, err := chain.RMNRemote.Curse0(deployer, subjectsByte16) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to curse chain %d: %w", selector, err) + } + } + } + + return deployerGroup.enact(fmt.Sprintf("proposal to curse RMNs: %s", cfg.CurseReason)) +} diff --git a/deployment/ccip/changeset/cs_rmn_curse_uncurse_test.go b/deployment/ccip/changeset/cs_rmn_curse_uncurse_test.go new file mode 100644 index 00000000000..5682be39811 --- /dev/null +++ b/deployment/ccip/changeset/cs_rmn_curse_uncurse_test.go @@ -0,0 +1,61 @@ +package changeset + +import ( + "testing" +) + +type curseAssertion struct { + chainSelector uint64 + subject uint64 +} + +type CurseTestCase struct { + useMCMS bool + name string + curseActionsBuilder []CurseAction + curseAssertions []curseAssertion +} + +func TestRMNCurse(t *testing.T) { + t.Parallel() + testCases := []CurseTestCase{ + // { + // useMCMS: true, + // name: "with MCMS", + // }, + { + useMCMS: false, + name: "without MCMS", + curseActionsBuilder: []CurseAction{CurseLane(0, 1)}, + curseAssertions: []curseAssertion{ + {chainSelector: 0, subject: 1}, + {chainSelector: 1, subject: 0}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + testRmnCurse(t, tc) + }) + } +} + +func testRmnCurse(t *testing.T, tc CurseTestCase) { + e := NewMemoryEnvironment(t, WithChains(2)) + + config := RMNCurseConfig{ + HomeChainSelector: e.HomeChainSel, + CurseActions: tc.curseActionsBuilder, + CurseReason: "test curse", + } + + if tc.useMCMS { + config.MCMS = &MCMSConfig{ + MinDelay: 0, + } + } + + NewRMNCurseChangeset(e.Env, config) +} diff --git a/deployment/ccip/changeset/cs_update_rmn_config.go b/deployment/ccip/changeset/cs_update_rmn_config.go index 337b3756881..e26342eb494 100644 --- a/deployment/ccip/changeset/cs_update_rmn_config.go +++ b/deployment/ccip/changeset/cs_update_rmn_config.go @@ -9,7 +9,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms" "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" @@ -408,34 +407,6 @@ func NewPromoteCandidateConfigChangeset(e deployment.Environment, config Promote }, nil } -func buildTimelockPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]*proposalutils.TimelockExecutionContracts { - timelocksPerChain := make(map[uint64]*proposalutils.TimelockExecutionContracts) - for _, chain := range e.Chains { - timelocksPerChain[chain.Selector] = &proposalutils.TimelockExecutionContracts{ - Timelock: state.Chains[chain.Selector].Timelock, - CallProxy: state.Chains[chain.Selector].CallProxy, - } - } - return timelocksPerChain -} - -func buildTimelockAddressPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]common.Address { - timelocksPerChain := buildTimelockPerChain(e, state) - timelockAddressPerChain := make(map[uint64]common.Address) - for chain, timelock := range timelocksPerChain { - timelockAddressPerChain[chain] = timelock.Timelock.Address() - } - return timelockAddressPerChain -} - -func buildProposerPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]*gethwrappers.ManyChainMultiSig { - proposerPerChain := make(map[uint64]*gethwrappers.ManyChainMultiSig) - for _, chain := range e.Chains { - proposerPerChain[chain.Selector] = state.Chains[chain.Selector].ProposerMcm - } - return proposerPerChain -} - func buildRMNRemotePerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]*rmn_remote.RMNRemote { timelocksPerChain := make(map[uint64]*rmn_remote.RMNRemote) for _, chain := range e.Chains { diff --git a/deployment/ccip/changeset/deployer_group.go b/deployment/ccip/changeset/deployer_group.go new file mode 100644 index 00000000000..2b831b38497 --- /dev/null +++ b/deployment/ccip/changeset/deployer_group.go @@ -0,0 +1,131 @@ +package changeset + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" +) + +type DeployerGroup struct { + e deployment.Environment + state CCIPOnChainState + mcmConfig *MCMSConfig + transactions map[uint64][]*types.Transaction +} + +func NewDeployerGroup(e deployment.Environment, state CCIPOnChainState, mcmConfig *MCMSConfig) *DeployerGroup { + return &DeployerGroup{ + e: e, + mcmConfig: mcmConfig, + state: state, + transactions: make(map[uint64][]*types.Transaction), + } +} + +func (d *DeployerGroup) getDeployer(chain uint64) *bind.TransactOpts { + sim := deployment.SimTransactOpts() + oldSigner := sim.Signer + sim.Signer = func(a common.Address, t *types.Transaction) (*types.Transaction, error) { + tx, err := oldSigner(a, t) + if err != nil { + return nil, err + } + d.transactions[chain] = append(d.transactions[chain], tx) + return tx, nil + } + return sim +} + +func (d *DeployerGroup) enact(deploymentDescription string) (deployment.ChangesetOutput, error) { + if d.mcmConfig != nil { + return d.enactMcms(deploymentDescription) + } else { + return d.enactDeployer() + } +} + +func (d *DeployerGroup) enactMcms(deploymentDescription string) (deployment.ChangesetOutput, error) { + batches := make([]timelock.BatchChainOperation, 0) + for selector, txs := range d.transactions { + mcmOps := make([]mcms.Operation, len(txs)) + for i, tx := range txs { + mcmOps[i] = mcms.Operation{ + To: *tx.To(), + Data: tx.Data(), + Value: tx.Value(), + } + } + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(selector), + Batch: mcmOps, + }) + } + + timelocksPerChain := buildTimelockAddressPerChain(d.e, d.state) + + proposerMCMSes := buildProposerPerChain(d.e, d.state) + + prop, err := proposalutils.BuildProposalFromBatches( + timelocksPerChain, + proposerMCMSes, + batches, + deploymentDescription, + d.mcmConfig.MinDelay, + ) + + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal %w", err) + } + + return deployment.ChangesetOutput{ + Proposals: []timelock.MCMSWithTimelockProposal{*prop}, + }, nil +} + +func (d *DeployerGroup) enactDeployer() (deployment.ChangesetOutput, error) { + for selector, txs := range d.transactions { + for _, tx := range txs { + err := d.e.Chains[selector].Client.SendTransaction(context.Background(), tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to send transaction: %w", err) + } + } + } + return deployment.ChangesetOutput{}, nil +} + +func buildTimelockPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]*proposalutils.TimelockExecutionContracts { + timelocksPerChain := make(map[uint64]*proposalutils.TimelockExecutionContracts) + for _, chain := range e.Chains { + timelocksPerChain[chain.Selector] = &proposalutils.TimelockExecutionContracts{ + Timelock: state.Chains[chain.Selector].Timelock, + CallProxy: state.Chains[chain.Selector].CallProxy, + } + } + return timelocksPerChain +} + +func buildTimelockAddressPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]common.Address { + timelocksPerChain := buildTimelockPerChain(e, state) + timelockAddressPerChain := make(map[uint64]common.Address) + for chain, timelock := range timelocksPerChain { + timelockAddressPerChain[chain] = timelock.Timelock.Address() + } + return timelockAddressPerChain +} + +func buildProposerPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]*gethwrappers.ManyChainMultiSig { + proposerPerChain := make(map[uint64]*gethwrappers.ManyChainMultiSig) + for _, chain := range e.Chains { + proposerPerChain[chain.Selector] = state.Chains[chain.Selector].ProposerMcm + } + return proposerPerChain +}