Skip to content

Commit

Permalink
feat: keystone ownership transfer [KS-558] (#15451)
Browse files Browse the repository at this point in the history
* wip

* add test

* make the proposal multichain

* add transfer ownership changeset

use these changesets in other tests and axe duplicate code

* fix add_chain_test.go

* extract common code to func

* move changeset to common

Refactor the proposal helpers a bit

* move transfer ownership cs to common

* fix

* feat: add transfer and accept ownership changesets for keystone

* chore: rename to OwnersPerChain

* feat: add changeset for feeds consumer deployment and update ownership changesets with it.

* feat: modify ownership changesets to work on multiple contracts per chain.

* fix: comment

* Update deployment/common/changeset/transfer_ownership.go

Co-authored-by: Graham Goh <graham.goh@smartcontract.com>

* fix: remove logger from deploy func in favor of contextualized logger on the deployment struct.

* fix: use variadic for better readability.

* fix: use variadic for appending, and add type assertions for functions signatures

---------

Co-authored-by: Makram Kamaleddine <makramkd@users.noreply.github.com>
Co-authored-by: Graham Goh <graham.goh@smartcontract.com>
  • Loading branch information
3 people authored Dec 3, 2024
1 parent 9deaaf5 commit 636e633
Show file tree
Hide file tree
Showing 23 changed files with 699 additions and 69 deletions.
16 changes: 9 additions & 7 deletions deployment/ccip/changeset/accept_ownership_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import (
"github.com/ethereum/go-ethereum/common"
"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"

"github.com/smartcontractkit/chainlink/v2/core/logger"
)

func Test_NewAcceptOwnershipChangeset(t *testing.T) {
Expand Down Expand Up @@ -120,8 +122,8 @@ func genTestTransferOwnershipConfig(
)

return commonchangeset.TransferOwnershipConfig{
TimelocksPerChain: timelocksPerChain,
Contracts: contracts,
OwnersPerChain: timelocksPerChain,
Contracts: contracts,
}
}

Expand Down Expand Up @@ -158,10 +160,10 @@ func genTestAcceptOwnershipConfig(
)

return commonchangeset.AcceptOwnershipConfig{
TimelocksPerChain: timelocksPerChain,
ProposerMCMSes: proposerMCMses,
Contracts: contracts,
MinDelay: time.Duration(0),
OwnersPerChain: timelocksPerChain,
ProposerMCMSes: proposerMCMses,
Contracts: contracts,
MinDelay: time.Duration(0),
}
}

Expand Down
13 changes: 7 additions & 6 deletions deployment/common/changeset/accept_ownership.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"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"
)
Expand All @@ -22,8 +23,8 @@ type OwnershipAcceptor interface {
}

type AcceptOwnershipConfig struct {
// TimelocksPerChain is a mapping from chain selector to the timelock contract address on that chain.
TimelocksPerChain map[uint64]common.Address
// OwnersPerChain is a mapping from chain selector to the owner contract address on that chain.
OwnersPerChain map[uint64]common.Address

// ProposerMCMSes is a mapping from chain selector to the proposer MCMS contract on that chain.
ProposerMCMSes map[uint64]*gethwrappers.ManyChainMultiSig
Expand All @@ -39,11 +40,11 @@ type AcceptOwnershipConfig struct {
}

func (a AcceptOwnershipConfig) Validate() error {
// check that we have timelocks and proposer mcmses for the chains
// check that we have owners and proposer mcmses for the chains
// in the Contracts field.
for chainSelector := range a.Contracts {
if _, ok := a.TimelocksPerChain[chainSelector]; !ok {
return fmt.Errorf("missing timelock for chain %d", chainSelector)
if _, ok := a.OwnersPerChain[chainSelector]; !ok {
return fmt.Errorf("missing owner for chain %d", chainSelector)
}
if _, ok := a.ProposerMCMSes[chainSelector]; !ok {
return fmt.Errorf("missing proposer MCMS for chain %d", chainSelector)
Expand Down Expand Up @@ -88,7 +89,7 @@ func NewAcceptOwnershipChangeset(
}

proposal, err := proposalutils.BuildProposalFromBatches(
cfg.TimelocksPerChain,
cfg.OwnersPerChain,
cfg.ProposerMCMSes,
batches,
"Accept ownership of contracts",
Expand Down
9 changes: 5 additions & 4 deletions deployment/common/changeset/accept_ownership_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers"
"github.com/smartcontractkit/chainlink/deployment/common/changeset"
"github.com/stretchr/testify/assert"

"github.com/smartcontractkit/chainlink/deployment/common/changeset"
)

func TestAcceptOwnershipConfig_Validate(t *testing.T) {
Expand All @@ -19,7 +20,7 @@ func TestAcceptOwnershipConfig_Validate(t *testing.T) {
{
name: "valid config",
config: changeset.AcceptOwnershipConfig{
TimelocksPerChain: map[uint64]common.Address{
OwnersPerChain: map[uint64]common.Address{
1: common.HexToAddress("0x1"),
},
ProposerMCMSes: map[uint64]*gethwrappers.ManyChainMultiSig{
Expand All @@ -35,7 +36,7 @@ func TestAcceptOwnershipConfig_Validate(t *testing.T) {
{
name: "missing timelock",
config: changeset.AcceptOwnershipConfig{
TimelocksPerChain: map[uint64]common.Address{},
OwnersPerChain: map[uint64]common.Address{},
ProposerMCMSes: map[uint64]*gethwrappers.ManyChainMultiSig{
1: {},
},
Expand All @@ -49,7 +50,7 @@ func TestAcceptOwnershipConfig_Validate(t *testing.T) {
{
name: "missing proposer MCMS",
config: changeset.AcceptOwnershipConfig{
TimelocksPerChain: map[uint64]common.Address{
OwnersPerChain: map[uint64]common.Address{
1: common.HexToAddress("0x1"),
},
ProposerMCMSes: map[uint64]*gethwrappers.ManyChainMultiSig{},
Expand Down
21 changes: 11 additions & 10 deletions deployment/common/changeset/transfer_ownership.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"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"
)

Expand All @@ -15,18 +16,18 @@ type OwnershipTransferrer interface {
}

type TransferOwnershipConfig struct {
// TimelocksPerChain is a mapping from chain selector to the timelock contract address on that chain.
TimelocksPerChain map[uint64]common.Address
// OwnersPerChain is a mapping from chain selector to the owner's contract address on that chain.
OwnersPerChain 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.
// check that we have owners 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)
if _, ok := t.OwnersPerChain[chainSelector]; !ok {
return fmt.Errorf("missing owners for chain %d", chainSelector)
}
}

Expand All @@ -36,8 +37,8 @@ func (t TransferOwnershipConfig) Validate() error {
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.
// contracts in the provided configuration to correct owner on that chain.
// If the owner is already the provided address, no transaction is sent.
func NewTransferOwnershipChangeset(
e deployment.Environment,
cfg TransferOwnershipConfig,
Expand All @@ -47,14 +48,14 @@ func NewTransferOwnershipChangeset(
}

for chainSelector, contracts := range cfg.Contracts {
timelock := cfg.TimelocksPerChain[chainSelector]
ownerAddress := cfg.OwnersPerChain[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)
if owner != ownerAddress {
tx, err := contract.TransferOwnership(e.Chains[chainSelector].DeployerKey, ownerAddress)
_, 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)
Expand Down
8 changes: 6 additions & 2 deletions deployment/keystone/capability_registry_deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ type CapabilitiesRegistryDeployer struct {
contract *capabilities_registry.CapabilitiesRegistry
}

func NewCapabilitiesRegistryDeployer(lggr logger.Logger) *CapabilitiesRegistryDeployer {
return &CapabilitiesRegistryDeployer{lggr: lggr}
func NewCapabilitiesRegistryDeployer() (*CapabilitiesRegistryDeployer, error) {
lggr, err := logger.New()
if err != nil {
return nil, err
}
return &CapabilitiesRegistryDeployer{lggr: lggr}, nil
}

func (c *CapabilitiesRegistryDeployer) Contract() *capabilities_registry.CapabilitiesRegistry {
Expand Down
89 changes: 89 additions & 0 deletions deployment/keystone/changeset/accept_ownership.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package changeset

import (
"time"

"github.com/ethereum/go-ethereum/common"

ccipowner "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers"

"github.com/smartcontractkit/chainlink/deployment"
"github.com/smartcontractkit/chainlink/deployment/common/changeset"
)

func toOwnershipAcceptors[T changeset.OwnershipAcceptor](items []T) []changeset.OwnershipAcceptor {
ownershipAcceptors := make([]changeset.OwnershipAcceptor, len(items))
for i, item := range items {
ownershipAcceptors[i] = item
}
return ownershipAcceptors
}

type AcceptAllOwnershipRequest struct {
ChainSelector uint64
MinDelay time.Duration
}

var _ deployment.ChangeSet[*AcceptAllOwnershipRequest] = AcceptAllOwnershipsProposal

// AcceptAllOwnershipsProposal creates a MCMS proposal to call accept ownership on all the Keystone contracts in the address book.
func AcceptAllOwnershipsProposal(e deployment.Environment, req *AcceptAllOwnershipRequest) (deployment.ChangesetOutput, error) {
chainSelector := req.ChainSelector
minDelay := req.MinDelay
chain := e.Chains[chainSelector]
addrBook := e.ExistingAddresses

// Fetch contracts from the address book.
timelocks, err := timelocksFromAddrBook(addrBook, chain)
if err != nil {
return deployment.ChangesetOutput{}, err
}
capRegs, err := capRegistriesFromAddrBook(addrBook, chain)
if err != nil {
return deployment.ChangesetOutput{}, err
}
ocr3, err := ocr3FromAddrBook(addrBook, chain)
if err != nil {
return deployment.ChangesetOutput{}, err
}
forwarders, err := forwardersFromAddrBook(addrBook, chain)
if err != nil {
return deployment.ChangesetOutput{}, err
}
consumers, err := feedsConsumersFromAddrBook(addrBook, chain)
if err != nil {
return deployment.ChangesetOutput{}, err
}
mcmsProposers, err := proposersFromAddrBook(addrBook, chain)
if err != nil {
return deployment.ChangesetOutput{}, err
}

// Initialize the OwnershipAcceptors slice
var ownershipAcceptors []changeset.OwnershipAcceptor

// Append all contracts
ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(capRegs)...)
ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(ocr3)...)
ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(forwarders)...)
ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(consumers)...)

// Construct the configuration
cfg := changeset.AcceptOwnershipConfig{
OwnersPerChain: map[uint64]common.Address{
// Assuming there is only one timelock per chain.
chainSelector: timelocks[0].Address(),
},
ProposerMCMSes: map[uint64]*ccipowner.ManyChainMultiSig{
// Assuming there is only one MCMS proposer per chain.
chainSelector: mcmsProposers[0],
},
Contracts: map[uint64][]changeset.OwnershipAcceptor{
chainSelector: ownershipAcceptors,
},
MinDelay: minDelay,
}

// Create and return the changeset
return changeset.NewAcceptOwnershipChangeset(e, cfg)
}
86 changes: 86 additions & 0 deletions deployment/keystone/changeset/accept_ownership_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package changeset_test

import (
"math/big"
"testing"
"time"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"

commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset"
"github.com/smartcontractkit/chainlink/deployment/common/types"
"github.com/smartcontractkit/chainlink/deployment/environment/memory"
"github.com/smartcontractkit/chainlink/deployment/keystone/changeset"
)

func TestAcceptAllOwnership(t *testing.T) {
t.Parallel()
lggr := logger.Test(t)
cfg := memory.MemoryEnvironmentConfig{
Nodes: 1,
Chains: 2,
}
env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg)
registrySel := env.AllChainSelectors()[0]
chCapReg, err := changeset.DeployCapabilityRegistry(env, registrySel)
require.NoError(t, err)
require.NotNil(t, chCapReg)
err = env.ExistingAddresses.Merge(chCapReg.AddressBook)
require.NoError(t, err)

chOcr3, err := changeset.DeployOCR3(env, registrySel)
require.NoError(t, err)
require.NotNil(t, chOcr3)
err = env.ExistingAddresses.Merge(chOcr3.AddressBook)
require.NoError(t, err)

chForwarder, err := changeset.DeployForwarder(env, registrySel)
require.NoError(t, err)
require.NotNil(t, chForwarder)
err = env.ExistingAddresses.Merge(chForwarder.AddressBook)
require.NoError(t, err)

chConsumer, err := changeset.DeployFeedsConsumer(env, &changeset.DeployFeedsConsumerRequest{
ChainSelector: registrySel,
})
require.NoError(t, err)
require.NotNil(t, chConsumer)
err = env.ExistingAddresses.Merge(chConsumer.AddressBook)
require.NoError(t, err)

chMcms, err := commonchangeset.DeployMCMSWithTimelock(env, map[uint64]types.MCMSWithTimelockConfig{
registrySel: {
Canceller: commonchangeset.SingleGroupMCMS(t),
Bypasser: commonchangeset.SingleGroupMCMS(t),
Proposer: commonchangeset.SingleGroupMCMS(t),
TimelockExecutors: env.AllDeployerKeys(),
TimelockMinDelay: big.NewInt(0),
},
})
err = env.ExistingAddresses.Merge(chMcms.AddressBook)
require.NoError(t, err)

require.NoError(t, err)
require.NotNil(t, chMcms)

resp, err := changeset.TransferAllOwnership(env, &changeset.TransferAllOwnershipRequest{
ChainSelector: registrySel,
})
require.NoError(t, err)
require.NotNil(t, resp)

// Test the changeset
output, err := changeset.AcceptAllOwnershipsProposal(env, &changeset.AcceptAllOwnershipRequest{
ChainSelector: registrySel,
MinDelay: time.Duration(0),
})
require.NoError(t, err)
require.NotNil(t, output)
require.Len(t, output.Proposals, 1)
proposal := output.Proposals[0]
require.Len(t, proposal.Transactions, 1)
txs := proposal.Transactions[0]
require.Len(t, txs.Batch, 4)
}
Loading

0 comments on commit 636e633

Please sign in to comment.