Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deployment/ccip/changeset: add transfer/accept ownership changeset #15409

Merged
merged 14 commits into from
Nov 28, 2024
149 changes: 149 additions & 0 deletions deployment/ccip/changeset/accept_ownership.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package changeset

import (
"fmt"
"math/big"
"time"

"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/ccip-owner-contracts/pkg/proposal/mcms"
"github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock"
"github.com/smartcontractkit/chainlink/deployment"
)

type ownershipAcceptor interface {
AcceptOwnership(opts *bind.TransactOpts) (*gethtypes.Transaction, error)
Address() common.Address
}

type AcceptOwnershipConfig struct {
State CCIPOnChainState
makramkd marked this conversation as resolved.
Show resolved Hide resolved
ChainSelectors []uint64
makramkd marked this conversation as resolved.
Show resolved Hide resolved
HomeChainSelector uint64
}

// type assertion - comply with deployment.ChangeSet interface
var _ deployment.ChangeSet[AcceptOwnershipConfig] = NewAcceptOwnershipChangeset

// NewAcceptOwnershipChangeset creates a changeset that accepts ownership of all the
// ccip contracts deployed on the given chain selectors.
// New chain contracts are:
// * OnRamp
// * OffRamp
// * FeeQuoter
// * NonceManager
// * RMNRemote
// Home chain contracts are:
// * CCIPHome
// * RMNHome
// * CapabilityRegistry
func NewAcceptOwnershipChangeset(
e deployment.Environment,
cfg AcceptOwnershipConfig,
) (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")
}
makramkd marked this conversation as resolved.
Show resolved Hide resolved

// gen one batch per chain for the chain contracts
var batches []timelock.BatchChainOperation
for _, chainSelector := range cfg.ChainSelectors {
chainState, ok := cfg.State.Chains[chainSelector]
if !ok {
return deployment.ChangesetOutput{}, fmt.Errorf("chain %d not found in state", chainSelector)
}

ops, err := genAcceptOwnershipOps(chainState)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate accept ownership batch for chain %d: %w",
chainSelector, err)
}

batches = append(batches, timelock.BatchChainOperation{
ChainIdentifier: mcms.ChainIdentifier(chainSelector),
Batch: ops,
})
}

// gen separate batch chain operation for home chain contracts
homeChainState, ok := cfg.State.Chains[cfg.HomeChainSelector]
if !ok {
return deployment.ChangesetOutput{}, fmt.Errorf("home chain %d not found in state", cfg.HomeChainSelector)
}
homeChainOps, err := genHomeChainAcceptOwnershipOps(homeChainState)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate accept ownership batch for home chain %d: %w",
cfg.HomeChainSelector, err)
}

batches = append(batches, timelock.BatchChainOperation{
ChainIdentifier: mcms.ChainIdentifier(cfg.HomeChainSelector),
Batch: homeChainOps,
})

proposal, err := BuildProposalFromBatches(
cfg.State,
batches,
"Accept ownership of all CCIP contracts",
time.Duration(0), // minDelay
)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal from batch: %w, batches: %+v", err, batches)
}

return deployment.ChangesetOutput{
Proposals: []timelock.MCMSWithTimelockProposal{*proposal},
}, nil
}

func genAcceptOwnershipOps(chainState CCIPChainState) (ops []mcms.Operation, err error) {
for _, contract := range []ownershipAcceptor{
chainState.OnRamp,
chainState.OffRamp,
chainState.FeeQuoter,
chainState.NonceManager,
chainState.RMNRemote,
} {
acceptOwnershipTx, err := contract.AcceptOwnership(deployment.SimTransactOpts())
if err != nil {
return nil, fmt.Errorf("failed to generate accept ownership calldata of %T: %w", contract, err)
}

ops = append(ops, mcms.Operation{
To: contract.Address(),
Data: acceptOwnershipTx.Data(),
Value: big.NewInt(0),
})
}

return ops, nil
}

func genHomeChainAcceptOwnershipOps(homeChainState CCIPChainState) (ops []mcms.Operation, err error) {
for _, contract := range []ownershipAcceptor{
homeChainState.CapabilityRegistry,
homeChainState.CCIPHome,
homeChainState.RMNHome,
} {
acceptOwnershipTx, err := contract.AcceptOwnership(deployment.SimTransactOpts())
if err != nil {
return nil, fmt.Errorf("failed to generate accept ownership calldata of %T: %w", contract, err)
}

ops = append(ops, mcms.Operation{
To: contract.Address(),
Data: acceptOwnershipTx.Data(),
Value: big.NewInt(0),
})
}

return ops, nil
}
136 changes: 136 additions & 0 deletions deployment/ccip/changeset/accept_ownership_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package changeset

import (
"math/big"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers"
"github.com/smartcontractkit/chainlink-common/pkg/utils/tests"
"github.com/smartcontractkit/chainlink/deployment"
commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset"
commontypes "github.com/smartcontractkit/chainlink/deployment/common/types"

"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
)

func Test_NewAcceptOwnershipChangeset(t *testing.T) {
e := NewMemoryEnvironmentWithJobs(t, logger.TestLogger(t), 2, 4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can just use a generic test for this if you are moving to common changeset

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept the CCIP test but moved the changeset to common

state, err := LoadOnchainState(e.Env)
require.NoError(t, err)

allChains := maps.Keys(e.Env.Chains)
source := allChains[0]
dest := allChains[1]

newAddresses := deployment.NewMemoryAddressBook()
err = deployPrerequisiteChainContracts(e.Env, newAddresses, allChains, nil)
require.NoError(t, err)
require.NoError(t, e.Env.ExistingAddresses.Merge(newAddresses))

mcmConfig := commontypes.MCMSWithTimelockConfig{
Canceller: commonchangeset.SingleGroupMCMS(t),
Bypasser: commonchangeset.SingleGroupMCMS(t),
Proposer: commonchangeset.SingleGroupMCMS(t),
TimelockExecutors: e.Env.AllDeployerKeys(),
TimelockMinDelay: big.NewInt(0),
}
out, err := commonchangeset.DeployMCMSWithTimelock(e.Env, map[uint64]commontypes.MCMSWithTimelockConfig{
source: mcmConfig,
dest: mcmConfig,
})
require.NoError(t, err)
require.NoError(t, e.Env.ExistingAddresses.Merge(out.AddressBook))
newAddresses = deployment.NewMemoryAddressBook()
tokenConfig := NewTestTokenConfig(state.Chains[e.FeedChainSel].USDFeeds)
ocrParams := make(map[uint64]CCIPOCRParams)
for _, chain := range allChains {
ocrParams[chain] = DefaultOCRParams(e.FeedChainSel, nil, nil)
}
err = deployCCIPContracts(e.Env, newAddresses, NewChainsConfig{
HomeChainSel: e.HomeChainSel,
FeedChainSel: e.FeedChainSel,
ChainsToDeploy: allChains,
TokenConfig: tokenConfig,
OCRSecrets: deployment.XXXGenerateTestOCRSecrets(),
OCRParams: ocrParams,
})
require.NoError(t, err)

// at this point we have the initial deploys done, now we need to transfer ownership
// to the timelock contract
state, err = LoadOnchainState(e.Env)
require.NoError(t, err)

// compose the transfer ownership and accept ownership changesets
_, err = commonchangeset.ApplyChangesets(t, e.Env, map[uint64]*gethwrappers.RBACTimelock{
source: state.Chains[source].Timelock,
dest: state.Chains[dest].Timelock,
}, []commonchangeset.ChangesetApplication{
// note this doesn't have proposals.
{
Changeset: commonchangeset.WrapChangeSet(NewTransferOwnershipChangeset),
Config: TransferOwnershipConfig{
State: state,
ChainSelectors: allChains,
HomeChainSelector: e.HomeChainSel,
},
},
// this has proposals, ApplyChangesets will sign & execute them.
// in practice, signing and executing are separated processes.
{
Changeset: commonchangeset.WrapChangeSet(NewAcceptOwnershipChangeset),
Config: AcceptOwnershipConfig{
State: state,
ChainSelectors: allChains,
HomeChainSelector: e.HomeChainSel,
},
},
})
require.NoError(t, err)

assertTimelockOwnership(t, e, allChains, state)
}

// assertTimelockOwnership asserts that the ownership of the contracts has been transferred
// to the appropriate timelock contract on each chain.
func assertTimelockOwnership(
t *testing.T,
e DeployedEnv,
chains []uint64,
state CCIPOnChainState,
) {
ctx := tests.Context(t)
// check that the ownership has been transferred correctly
for _, chain := range chains {
for _, contract := range []ownershipTransferrer{
state.Chains[chain].OnRamp,
state.Chains[chain].OffRamp,
state.Chains[chain].FeeQuoter,
state.Chains[chain].NonceManager,
state.Chains[chain].RMNRemote,
} {
owner, err := contract.Owner(&bind.CallOpts{
Context: ctx,
})
require.NoError(t, err)
require.Equal(t, state.Chains[chain].Timelock.Address(), owner)
}
}

// check home chain contracts ownership
homeChainTimelockAddress := state.Chains[e.HomeChainSel].Timelock.Address()
for _, contract := range []ownershipTransferrer{
state.Chains[e.HomeChainSel].CapabilityRegistry,
state.Chains[e.HomeChainSel].CCIPHome,
state.Chains[e.HomeChainSel].RMNHome,
} {
owner, err := contract.Owner(&bind.CallOpts{
Context: ctx,
})
require.NoError(t, err)
require.Equal(t, homeChainTimelockAddress, owner)
}
}
36 changes: 29 additions & 7 deletions deployment/ccip/changeset/active_candidate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"testing"

"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"
"golang.org/x/exp/maps"

"github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext"

Expand All @@ -30,6 +32,7 @@ func TestActiveCandidate(t *testing.T) {
e := tenv.Env
state, err := LoadOnchainState(tenv.Env)
require.NoError(t, err)
allChains := maps.Keys(e.Chains)

// Add all lanes
require.NoError(t, AddLanesForAll(e, state))
Expand Down Expand Up @@ -80,14 +83,33 @@ func TestActiveCandidate(t *testing.T) {
//Wait for all exec reports to land
ConfirmExecWithSeqNrsForAll(t, e, state, expectedSeqNumExec, startBlocks)

// transfer ownership
TransferAllOwnership(t, state, tenv.HomeChainSel, e)
acceptOwnershipProposal, err := GenerateAcceptOwnershipProposal(state, tenv.HomeChainSel, e.AllChainSelectors())
require.NoError(t, err)
acceptOwnershipExec := commonchangeset.SignProposal(t, e, acceptOwnershipProposal)
for _, sel := range e.AllChainSelectors() {
commonchangeset.ExecuteProposal(t, e, acceptOwnershipExec, state.Chains[sel].Timelock, sel)
// compose the transfer ownership and accept ownership changesets
timelocks := make(map[uint64]*gethwrappers.RBACTimelock)
for _, chain := range allChains {
timelocks[chain] = state.Chains[chain].Timelock
}
_, 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,
},
},
// this has proposals, ApplyChangesets will sign & execute them.
// in practice, signing and executing are separated processes.
{
Changeset: commonchangeset.WrapChangeSet(NewAcceptOwnershipChangeset),
Config: AcceptOwnershipConfig{
State: state,
ChainSelectors: allChains,
HomeChainSelector: tenv.HomeChainSel,
},
},
})
require.NoError(t, err)
// Apply the accept ownership proposal to all the chains.

err = ConfirmRequestOnSourceAndDest(t, e, state, tenv.HomeChainSel, tenv.FeedChainSel, 2)
Expand Down
Loading
Loading