diff --git a/deployment/ccip/changeset/accept_ownership_test.go b/deployment/ccip/changeset/accept_ownership_test.go index eab8a27b855..2be0b0315f3 100644 --- a/deployment/ccip/changeset/accept_ownership_test.go +++ b/deployment/ccip/changeset/accept_ownership_test.go @@ -73,12 +73,8 @@ func Test_NewAcceptOwnershipChangeset(t *testing.T) { }, []commonchangeset.ChangesetApplication{ // note this doesn't have proposals. { - Changeset: commonchangeset.WrapChangeSet(NewTransferOwnershipChangeset), - Config: TransferOwnershipConfig{ - State: state, - ChainSelectors: allChains, - HomeChainSelector: e.HomeChainSel, - }, + Changeset: commonchangeset.WrapChangeSet(commonchangeset.NewTransferOwnershipChangeset), + Config: genTestTransferOwnershipConfig(e, allChains, state), }, // this has proposals, ApplyChangesets will sign & execute them. // in practice, signing and executing are separated processes. @@ -92,6 +88,43 @@ func Test_NewAcceptOwnershipChangeset(t *testing.T) { assertTimelockOwnership(t, e, allChains, state) } +func genTestTransferOwnershipConfig( + e DeployedEnv, + chains []uint64, + state CCIPOnChainState, +) commonchangeset.TransferOwnershipConfig { + var ( + timelocksPerChain = make(map[uint64]common.Address) + contracts = make(map[uint64][]commonchangeset.OwnershipTransferrer) + ) + + // chain contracts + for _, chain := range chains { + timelocksPerChain[chain] = state.Chains[chain].Timelock.Address() + contracts[chain] = []commonchangeset.OwnershipTransferrer{ + state.Chains[chain].OnRamp, + state.Chains[chain].OffRamp, + state.Chains[chain].FeeQuoter, + state.Chains[chain].NonceManager, + state.Chains[chain].RMNRemote, + } + } + + // home chain + homeChainTimelockAddress := state.Chains[e.HomeChainSel].Timelock.Address() + timelocksPerChain[e.HomeChainSel] = homeChainTimelockAddress + contracts[e.HomeChainSel] = append(contracts[e.HomeChainSel], + state.Chains[e.HomeChainSel].CapabilityRegistry, + state.Chains[e.HomeChainSel].CCIPHome, + state.Chains[e.HomeChainSel].RMNHome, + ) + + return commonchangeset.TransferOwnershipConfig{ + TimelocksPerChain: timelocksPerChain, + Contracts: contracts, + } +} + func genTestAcceptOwnershipConfig( e DeployedEnv, chains []uint64, @@ -143,7 +176,7 @@ func assertTimelockOwnership( ctx := tests.Context(t) // check that the ownership has been transferred correctly for _, chain := range chains { - for _, contract := range []ownershipTransferrer{ + for _, contract := range []commonchangeset.OwnershipTransferrer{ state.Chains[chain].OnRamp, state.Chains[chain].OffRamp, state.Chains[chain].FeeQuoter, @@ -160,7 +193,7 @@ func assertTimelockOwnership( // check home chain contracts ownership homeChainTimelockAddress := state.Chains[e.HomeChainSel].Timelock.Address() - for _, contract := range []ownershipTransferrer{ + for _, contract := range []commonchangeset.OwnershipTransferrer{ state.Chains[e.HomeChainSel].CapabilityRegistry, state.Chains[e.HomeChainSel].CCIPHome, state.Chains[e.HomeChainSel].RMNHome, diff --git a/deployment/ccip/changeset/active_candidate_test.go b/deployment/ccip/changeset/active_candidate_test.go index e4485fe80d6..4d72ab6ccf2 100644 --- a/deployment/ccip/changeset/active_candidate_test.go +++ b/deployment/ccip/changeset/active_candidate_test.go @@ -92,12 +92,8 @@ func TestActiveCandidate(t *testing.T) { _, err = commonchangeset.ApplyChangesets(t, e, timelocks, []commonchangeset.ChangesetApplication{ // note this doesn't have proposals. { - Changeset: commonchangeset.WrapChangeSet(NewTransferOwnershipChangeset), - Config: TransferOwnershipConfig{ - State: state, - ChainSelectors: allChains, - HomeChainSelector: tenv.HomeChainSel, - }, + Changeset: commonchangeset.WrapChangeSet(commonchangeset.NewTransferOwnershipChangeset), + Config: genTestTransferOwnershipConfig(tenv, allChains, state), }, // this has proposals, ApplyChangesets will sign & execute them. // in practice, signing and executing are separated processes. diff --git a/deployment/ccip/changeset/add_chain_test.go b/deployment/ccip/changeset/add_chain_test.go index 83ebf56c345..9d6e175ddbc 100644 --- a/deployment/ccip/changeset/add_chain_test.go +++ b/deployment/ccip/changeset/add_chain_test.go @@ -132,12 +132,8 @@ func TestAddChainInbound(t *testing.T) { }, []commonchangeset.ChangesetApplication{ // note this doesn't have proposals. { - Changeset: commonchangeset.WrapChangeSet(NewTransferOwnershipChangeset), - Config: TransferOwnershipConfig{ - State: state, - ChainSelectors: initialDeploy, - HomeChainSelector: e.HomeChainSel, - }, + Changeset: commonchangeset.WrapChangeSet(commonchangeset.NewTransferOwnershipChangeset), + Config: genTestTransferOwnershipConfig(e, initialDeploy, state), }, // this has proposals, ApplyChangesets will sign & execute them. // in practice, signing and executing are separated processes. diff --git a/deployment/ccip/changeset/transfer_ownership.go b/deployment/ccip/changeset/transfer_ownership.go deleted file mode 100644 index 96b6714fa27..00000000000 --- a/deployment/ccip/changeset/transfer_ownership.go +++ /dev/null @@ -1,95 +0,0 @@ -package changeset - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - gethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/smartcontractkit/chainlink/deployment" -) - -type ownershipTransferrer interface { - TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*gethtypes.Transaction, error) - Owner(opts *bind.CallOpts) (common.Address, error) -} - -type TransferOwnershipConfig struct { - State CCIPOnChainState - ChainSelectors []uint64 - HomeChainSelector uint64 -} - -var _ deployment.ChangeSet[TransferOwnershipConfig] = NewTransferOwnershipChangeset - -// NewTransferOwnershipChangeset creates a changeset that transfers ownership of all the -// ccip chain contracts deployed on the given chain selectors. -// New chain contracts are: -// * OnRamp -// * OffRamp -// * FeeQuoter -// * NonceManager -// * RMNRemote -// Home chain contracts are: -// * CCIPHome -// * RMNHome -// * CapabilityRegistry -// This can be composed with NewAcceptOwnershipChangeset in order to fully transfer -// ownership of all the contracts listed above. -func NewTransferOwnershipChangeset( - e deployment.Environment, - cfg TransferOwnershipConfig, -) (deployment.ChangesetOutput, error) { - // basic validation - if len(cfg.ChainSelectors) == 0 || cfg.HomeChainSelector == 0 { - return deployment.ChangesetOutput{}, fmt.Errorf("no chain selectors provided") - } - - if len(cfg.State.Chains) == 0 { - return deployment.ChangesetOutput{}, fmt.Errorf("no chains in state") - } - - // transfer ownership of chain contracts - // these are assumed to be owned by the deployer configured in the given - // environment. - for _, chain := range cfg.ChainSelectors { - for _, contract := range []ownershipTransferrer{ - cfg.State.Chains[chain].OnRamp, - cfg.State.Chains[chain].OffRamp, - cfg.State.Chains[chain].FeeQuoter, - cfg.State.Chains[chain].NonceManager, - cfg.State.Chains[chain].RMNRemote, - } { - tx, err := contract.TransferOwnership( - e.Chains[chain].DeployerKey, - cfg.State.Chains[chain].Timelock.Address(), - ) - _, err = deployment.ConfirmIfNoError(e.Chains[chain], tx, err) - if err != nil { - return deployment.ChangesetOutput{}, err - } - } - } - - // transfer ownership of home chain contracts - homeChainTimelockAddress := cfg.State.Chains[cfg.HomeChainSelector].Timelock.Address() - for _, contract := range []ownershipTransferrer{ - cfg.State.Chains[cfg.HomeChainSelector].CapabilityRegistry, - cfg.State.Chains[cfg.HomeChainSelector].CCIPHome, - cfg.State.Chains[cfg.HomeChainSelector].RMNHome, - } { - tx, err := contract.TransferOwnership( - e.Chains[cfg.HomeChainSelector].DeployerKey, - homeChainTimelockAddress, - ) - _, err = deployment.ConfirmIfNoError(e.Chains[cfg.HomeChainSelector], tx, err) - if err != nil { - return deployment.ChangesetOutput{}, err - } - } - - // no new addresses or proposals or jobspecs, so changeset output is empty. - // NOTE: onchain state has technically changed for above contracts, maybe that should - // be captured? - return deployment.ChangesetOutput{}, nil -} diff --git a/deployment/common/changeset/transfer_ownership.go b/deployment/common/changeset/transfer_ownership.go new file mode 100644 index 00000000000..a0085fb61ca --- /dev/null +++ b/deployment/common/changeset/transfer_ownership.go @@ -0,0 +1,70 @@ +package changeset + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/smartcontractkit/chainlink/deployment" +) + +type OwnershipTransferrer interface { + TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*gethtypes.Transaction, error) + Owner(opts *bind.CallOpts) (common.Address, error) +} + +type TransferOwnershipConfig struct { + // TimelocksPerChain is a mapping from chain selector to the timelock contract address on that chain. + TimelocksPerChain map[uint64]common.Address + + // Contracts is a mapping from chain selector to the ownership transferrers on that chain. + Contracts map[uint64][]OwnershipTransferrer +} + +func (t TransferOwnershipConfig) Validate() error { + // check that we have timelocks for the chains in the Contracts field. + for chainSelector := range t.Contracts { + if _, ok := t.TimelocksPerChain[chainSelector]; !ok { + return fmt.Errorf("missing timelock for chain %d", chainSelector) + } + } + + return nil +} + +var _ deployment.ChangeSet[TransferOwnershipConfig] = NewTransferOwnershipChangeset + +// NewTransferOwnershipChangeset creates a changeset that transfers ownership of all the +// contracts in the provided configuration to the the appropriate timelock on that chain. +// If the owner is already the timelock contract, no transaction is sent. +func NewTransferOwnershipChangeset( + e deployment.Environment, + cfg TransferOwnershipConfig, +) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(); err != nil { + return deployment.ChangesetOutput{}, err + } + + for chainSelector, contracts := range cfg.Contracts { + timelock := cfg.TimelocksPerChain[chainSelector] + for _, contract := range contracts { + owner, err := contract.Owner(nil) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get owner of contract %T: %v", contract, err) + } + if owner != timelock { + tx, err := contract.TransferOwnership(e.Chains[chainSelector].DeployerKey, timelock) + _, err = deployment.ConfirmIfNoError(e.Chains[chainSelector], tx, err) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to transfer ownership of contract %T: %v", contract, err) + } + } + } + } + + // no new addresses or proposals or jobspecs, so changeset output is empty. + // NOTE: onchain state has technically changed for above contracts, maybe that should + // be captured? + return deployment.ChangesetOutput{}, nil +}