Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions chains/evm/deployment/v1_0_0/adapters/transfer_ownership_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,154 @@ import (
"github.com/smartcontractkit/chainlink-ccip/deployment/utils/mcms"
)

// TestTransferOwnershipAddressOnlyRef verifies that TransferOwnershipChangeset and
// AcceptOwnershipChangeset succeed when ContractRef contains only an Address with no
// Type/Version/Qualifier, and the contract is not present in the datastore. This exercises
// the best-effort DS enrichment path: DS miss is tolerated as long as Address is set.
Comment thread
chris-de-leon-cll marked this conversation as resolved.
func TestTransferOwnershipAddressOnlyRef(t *testing.T) {
Comment thread
chris-de-leon-cll marked this conversation as resolved.
t.Parallel()
selector := chainsel.TEST_90000001.Selector
env, err := environment.New(t.Context(),
environment.WithEVMSimulated(t, []uint64{selector}),
)
require.NoError(t, err)
env.Logger = logger.Test(t)

evmDeployer := &adapters.EVMDeployer{}
dReg := deploy.GetRegistry()
dReg.RegisterDeployer(chainsel.FamilyEVM, deploy.MCMSVersion, evmDeployer)
deployMCMS := deploy.DeployMCMS(dReg, nil)

// Deploy CLL MCMS set (used for the initial transfer).
output, err := deployMCMS.Apply(*env, deploy.MCMSDeploymentConfig{
AdapterVersion: semver.MustParse("1.0.0"),
MCMS: testhelpers.MCMSInputForQualifier(deploymentutils.CLLQualifier),
Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{
selector: {
Canceller: testhelpers.SingleGroupMCMS(),
Bypasser: testhelpers.SingleGroupMCMS(),
Proposer: testhelpers.SingleGroupMCMS(),
TimelockMinDelay: big.NewInt(1),
Qualifier: deploymentutils.StringPtr(deploymentutils.CLLQualifier),
},
},
})
require.NoError(t, err)
ds := output.DataStore
require.NoError(t, ds.Merge(env.DataStore))
env.DataStore = ds.Seal()
Comment thread
chris-de-leon-cll marked this conversation as resolved.

// Deploy RMN MCMS set (used as the accept-ownership target).
Comment thread
chris-de-leon-cll marked this conversation as resolved.
Outdated
output, err = deployMCMS.Apply(*env, deploy.MCMSDeploymentConfig{
AdapterVersion: semver.MustParse("1.0.0"),
MCMS: testhelpers.MCMSInputForQualifier(deploymentutils.CLLQualifier),
Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{
selector: {
Canceller: testhelpers.SingleGroupMCMS(),
Bypasser: testhelpers.SingleGroupMCMS(),
Proposer: testhelpers.SingleGroupMCMS(),
TimelockMinDelay: big.NewInt(1),
Qualifier: deploymentutils.StringPtr(deploymentutils.RMNTimelockQualifier),
},
},
})
require.NoError(t, err)
require.NoError(t, ds.Merge(output.DataStore.Seal()))
env.DataStore = ds.Seal()
Comment thread
chris-de-leon-cll marked this conversation as resolved.

// Deploy a router but intentionally do NOT add it to the datastore.
evmChain := env.BlockChains.EVMChains()[selector]
deployRouterOp, err := cldf_ops.ExecuteOperation(env.OperationsBundle, routerops1_2.Deploy, evmChain, contract.DeployInput[routerops1_2.ConstructorArgs]{
ChainSelector: evmChain.Selector,
TypeAndVersion: deployment.NewTypeAndVersion(routerops1_2.ContractType, *semver.MustParse("1.2.0")),
Args: routerops1_2.ConstructorArgs{
WrappedNative: utils.RandomAddress(),
RMNProxy: utils.RandomAddress(),
},
})
require.NoError(t, err)
routerAddr := common.HexToAddress(deployRouterOp.Output.Address)

cllTimelockRef, err := datastore_utils.FindAndFormatRef(env.DataStore, datastore.AddressRef{
ChainSelector: selector,
Type: datastore.ContractType(deploymentutils.RBACTimelock),
Qualifier: deploymentutils.CLLQualifier,
Version: semver.MustParse("1.0.0"),
}, selector, datastore_utils.FullRef)
require.NoError(t, err)

rmnTimelockRef, err := datastore_utils.FindAndFormatRef(env.DataStore, datastore.AddressRef{
ChainSelector: selector,
Type: datastore.ContractType(deploymentutils.RBACTimelock),
Qualifier: deploymentutils.RMNTimelockQualifier,
Version: semver.MustParse("1.0.0"),
}, selector, datastore_utils.FullRef)
require.NoError(t, err)

cr := deploy.GetTransferOwnershipRegistry()
evmAdapter := &adapters.EVMTransferOwnershipAdapter{}
cr.RegisterAdapter(chainsel.FamilyEVM, semver.MustParse("1.0.0"), evmAdapter)
mcmsRegistry := changesets.GetRegistry()
mcmsRegistry.RegisterMCMSReader(chainsel.FamilyEVM, &adapters.EVMMCMSReader{})

addrOnlyRef := []datastore.AddressRef{{Address: routerAddr.Hex()}}

// TransferOwnershipChangeset: transfer from deployer to CLL timelock using address-only ref.
transferOutput, err := deploy.TransferOwnershipChangeset(cr, mcmsRegistry).Apply(*env, deploy.TransferOwnershipInput{
AdapterVersion: semver.MustParse("1.0.0"),
MCMS: mcms.Input{
ValidUntil: 3759765795, TimelockAction: mcms_types.TimelockActionSchedule,
Qualifier: deploymentutils.CLLQualifier, Description: "transfer to CLL timelock",
},
ChainInputs: []deploy.TransferOwnershipPerChainInput{
{ChainSelector: selector, ContractRef: addrOnlyRef, ProposedOwner: cllTimelockRef.Address},
},
})
require.NoError(t, err)
require.Equal(t, 1, len(transferOutput.MCMSTimelockProposals))
testhelpers.ProcessTimelockProposals(t, *env, transferOutput.MCMSTimelockProposals, false)

r, err := router.NewRouter(routerAddr, evmChain.Client)
require.NoError(t, err)
owner, err := r.Owner(&bind.CallOpts{Context: t.Context()})
require.NoError(t, err)
require.Equal(t, common.HexToAddress(cllTimelockRef.Address), owner)

// Transfer ownership from CLL timelock to RMN timelock (sets up the pending transfer).
transferOutput, err = deploy.TransferOwnershipChangeset(cr, mcmsRegistry).Apply(*env, deploy.TransferOwnershipInput{
AdapterVersion: semver.MustParse("1.0.0"),
MCMS: mcms.Input{
ValidUntil: 3759765795, TimelockAction: mcms_types.TimelockActionSchedule,
Qualifier: deploymentutils.CLLQualifier, Description: "transfer to RMN timelock",
},
ChainInputs: []deploy.TransferOwnershipPerChainInput{
{ChainSelector: selector, ContractRef: addrOnlyRef, ProposedOwner: rmnTimelockRef.Address},
},
})
require.NoError(t, err)
require.Equal(t, 1, len(transferOutput.MCMSTimelockProposals))
testhelpers.ProcessTimelockProposals(t, *env, transferOutput.MCMSTimelockProposals, false)

// AcceptOwnershipChangeset: accept from RMN timelock using address-only ref.
acceptOutput, err := deploy.AcceptOwnershipChangeset(cr, mcmsRegistry).Apply(*env, deploy.TransferOwnershipInput{
AdapterVersion: semver.MustParse("1.0.0"),
MCMS: mcms.Input{
ValidUntil: 3759765795, TimelockAction: mcms_types.TimelockActionSchedule,
Qualifier: deploymentutils.RMNTimelockQualifier, Description: "accept from RMN timelock",
},
ChainInputs: []deploy.TransferOwnershipPerChainInput{
{ChainSelector: selector, ContractRef: addrOnlyRef, ProposedOwner: rmnTimelockRef.Address},
},
})
require.NoError(t, err)
require.Equal(t, 1, len(acceptOutput.MCMSTimelockProposals))
testhelpers.ProcessTimelockProposals(t, *env, acceptOutput.MCMSTimelockProposals, false)

owner, err = r.Owner(&bind.CallOpts{Context: t.Context()})
require.NoError(t, err)
require.Equal(t, common.HexToAddress(rmnTimelockRef.Address), owner)
}

// TestTransferOwnership tests transferring ownership of deployed contracts via MCMS timelocks.
// It deploys MCMS contracts on two EVM chains, deploys routers, transfers ownership of routers from deployer key to the timelock,
// then transfers ownership from the first timelock to a second timelock.
Expand Down
34 changes: 24 additions & 10 deletions deployment/deploy/transfer_ownership.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,16 @@ func acceptOwnershipApply(cr *TransferOwnershipAdapterRegistry, mcmsRegistry *ch
if err != nil {
return cldf.ChangesetOutput{}, err
}
// if partial refs are provided, resolve to full refs
for i, contractRef := range perChainInputs.ContractRef {
normRef, err := TryNormalizeAddressRef(perChainInputs.ChainSelector, contractRef)
addrRef, err := TryNormalizeAddressRef(perChainInputs.ChainSelector, contractRef)
if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to normalize contract ref %s for chain %d: %w", datastore_utils.SprintRef(contractRef), perChainInputs.ChainSelector, err)
}
fullRef, err := datastore_utils.FindAndFormatRef(e.DataStore, normRef, perChainInputs.ChainSelector, datastore_utils.FullRef)
addrRef, err = resolveContractRef(e, addrRef, perChainInputs.ChainSelector)
if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to resolve contract ref %s for chain %d: %w", datastore_utils.SprintRef(normRef), perChainInputs.ChainSelector, err)
return cldf.ChangesetOutput{}, err
}
perChainInputs.ContractRef[i] = fullRef
perChainInputs.ContractRef[i] = addrRef
}
report, err := cldf_ops.ExecuteSequence(e.OperationsBundle, adapter.SequenceAcceptOwnership(), e.BlockChains, perChainInputs)
if err != nil {
Expand Down Expand Up @@ -90,17 +89,16 @@ func transferOwnershipApply(cr *TransferOwnershipAdapterRegistry, mcmsRegistry *
if err != nil {
return cldf.ChangesetOutput{}, err
}
// if partial refs are provided, resolve to full refs
for i, contractRef := range perChainInputs.ContractRef {
normRef, err := TryNormalizeAddressRef(perChainInputs.ChainSelector, contractRef)
addrRef, err := TryNormalizeAddressRef(perChainInputs.ChainSelector, contractRef)
if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to normalize contract ref %s for chain %d: %w", datastore_utils.SprintRef(contractRef), perChainInputs.ChainSelector, err)
}
fullRef, err := datastore_utils.FindAndFormatRef(e.DataStore, normRef, perChainInputs.ChainSelector, datastore_utils.FullRef)
addrRef, err = resolveContractRef(e, addrRef, perChainInputs.ChainSelector)
if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to resolve contract ref %s for chain %d: %w", datastore_utils.SprintRef(normRef), perChainInputs.ChainSelector, err)
return cldf.ChangesetOutput{}, err
}
perChainInputs.ContractRef[i] = fullRef
perChainInputs.ContractRef[i] = addrRef
}
chainBatchOps, chainReports, err := transferAndAcceptOwnership(e, adapter, perChainInputs, input.MCMS)
if err != nil {
Expand Down Expand Up @@ -159,6 +157,22 @@ func TransferToTimelock(chainSel uint64, e cldf.Environment, mcmsInput mcms.Inpu
return transferAndAcceptOwnership(e, adapter, ownershipInput, mcmsInput)
}

// resolveContractRef attempts to enrich ref with full metadata from the datastore. If ref
// already has an Address and the DS lookup fails, the original ref is returned as-is (the
// address is sufficient for the ownership sequence; the `Type` is only used for logging).
// If ref has no Address, the DS lookup is required and a failure is returned as an error.
func resolveContractRef(e cldf.Environment, ref datastore.AddressRef, chainSelector uint64) (datastore.AddressRef, error) {
fullRef, err := datastore_utils.FindAndFormatRef(e.DataStore, ref, chainSelector, datastore_utils.FullRef)
if err != nil {
if ref.Address != "" {
e.Logger.Warnf("failed to resolve contract ref %s for chain %d - using provided ref instead: %v", datastore_utils.SprintRef(ref), chainSelector, err)
return ref, nil
}
return datastore.AddressRef{}, fmt.Errorf("failed to resolve contract ref %s for chain %d: %w", datastore_utils.SprintRef(ref), chainSelector, err)
}
return fullRef, nil
}
Comment thread
chris-de-leon-cll marked this conversation as resolved.
Outdated

// transferAndAcceptOwnership executes the transfer ownership sequence via MCMS and,
// if needed, the accept ownership sequence for the given contracts on a single chain.
func transferAndAcceptOwnership(
Expand Down
Loading