Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
54 changes: 54 additions & 0 deletions internal/testutil/datastoretest/datastoretest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Package datastoretest provides lightweight fakes for datastore interfaces in unit tests.
package datastoretest

import (
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
)

// NewDataStore returns a read-only DataStore backed by the given address refs.
func NewDataStore(refs []cldfdatastore.AddressRef) cldfdatastore.DataStore {
return fakeDataStore{store: fakeAddressRefStore{refs: refs}}
}

type fakeAddressRefStore struct {
refs []cldfdatastore.AddressRef
}

var _ cldfdatastore.AddressRefStore = fakeAddressRefStore{}

func (f fakeAddressRefStore) Fetch() ([]cldfdatastore.AddressRef, error) {
return f.refs, nil
}

func (f fakeAddressRefStore) Get(key cldfdatastore.AddressRefKey) (cldfdatastore.AddressRef, error) {
for _, ref := range f.refs {
if ref.Key().Equals(key) {
return ref, nil
}
}

return cldfdatastore.AddressRef{}, cldfdatastore.ErrAddressRefNotFound
}

func (f fakeAddressRefStore) Filter(filters ...cldfdatastore.FilterFunc[cldfdatastore.AddressRefKey, cldfdatastore.AddressRef]) []cldfdatastore.AddressRef {
refs := f.refs
for _, filter := range filters {
refs = filter(refs)
}

return refs
}

type fakeDataStore struct {
store fakeAddressRefStore
}

var _ cldfdatastore.DataStore = fakeDataStore{}

func (f fakeDataStore) Addresses() cldfdatastore.AddressRefStore { return f.store }

func (f fakeDataStore) ChainMetadata() cldfdatastore.ChainMetadataStore { return nil }

func (f fakeDataStore) ContractMetadata() cldfdatastore.ContractMetadataStore { return nil }

func (f fakeDataStore) EnvMetadata() cldfdatastore.EnvMetadataStore { return nil }
51 changes: 27 additions & 24 deletions mcms/evm/deploy/addresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package evmdeploy

import (
"fmt"

"github.com/ethereum/go-ethereum/common"
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
Expand All @@ -21,9 +23,9 @@ type deployedAddresses struct {
CallProxy common.Address
}

func loadDeployedAddresses(ds cldfdatastore.DataStore, chainSelector uint64, qualifier string) deployedAddresses {
func loadDeployedAddresses(ds cldfdatastore.DataStore, chainSelector uint64, qualifier string) (deployedAddresses, error) {
if ds == nil {
return deployedAddresses{}
return deployedAddresses{}, nil
}

type lookup struct {
Expand All @@ -41,45 +43,46 @@ func loadDeployedAddresses(ds cldfdatastore.DataStore, chainSelector uint64, qua
}

for _, l := range lookups {
if addr, ok := findDeployedAddress(ds.Addresses(), chainSelector, l.contractType, qualifier); ok {
addr, ok, err := findDeployedAddress(ds.Addresses(), chainSelector, l.contractType, qualifier)
if err != nil {
return deployedAddresses{}, err
}
if ok {
*l.dest = addr
}
}

return addrs
return addrs, nil
}

// findDeployedAddress returns a previously deployed contract address for this
// deploy package's version. Legacy datastore entries without a version are
// accepted as a fallback; refs with a different version are ignored so a
// version bump can redeploy.
// deploy package's version. Qualifier is always matched exactly, including "".
// Returns ok=false when no ref matches; an error when multiple refs match.
func findDeployedAddress(
store cldfdatastore.AddressRefStore,
chainSelector uint64,
contractType cldf.ContractType,
qualifier string,
) (common.Address, bool) {
baseFilters := make([]cldfdatastore.FilterFunc[cldfdatastore.AddressRefKey, cldfdatastore.AddressRef], 0, 3)
baseFilters = append(baseFilters,
) (common.Address, bool, error) {
version := semvers.V1_0_0

@graham-chainlink graham-chainlink Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

hardcoded version to be 1 for now, ideally user should specify the version they want (proposal util timelock config struct does not contain version field currently)

refs := store.Filter(
cldfdatastore.AddressRefByChainSelector(chainSelector),
cldfdatastore.AddressRefByType(cldfdatastore.ContractType(contractType)),
cldfdatastore.AddressRefByQualifier(qualifier),
cldfdatastore.AddressRefByVersion(&version),
)

version := semvers.V1_0_0
refs := store.Filter(append(baseFilters, cldfdatastore.AddressRefByVersion(&version))...)
if len(refs) > 0 {
return common.HexToAddress(refs[0].Address), true
switch len(refs) {
case 0:
return common.Address{}, false, nil
case 1:
return common.HexToAddress(refs[0].Address), true, nil
default:
return common.Address{}, false, fmt.Errorf(
"%w: chain selector %d contract type %s qualifier %q version %s: found %d refs",
cldfdatastore.ErrAddressRefQueryAmbiguous,
chainSelector, contractType, qualifier, version, len(refs),
)
}
Comment thread
graham-chainlink marked this conversation as resolved.

refs = store.Filter(baseFilters...)
for _, ref := range refs {
if ref.Version == nil {
return common.HexToAddress(ref.Address), true
}
}

return common.Address{}, false
}

// addressRefWithLabel attaches an optional label to a deployed contract address ref.
Expand Down
100 changes: 32 additions & 68 deletions mcms/evm/deploy/addresses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/cld-changesets/internal/semvers"
"github.com/smartcontractkit/cld-changesets/internal/testutil/datastoretest"
)

func TestLoadDeployedAddresses(t *testing.T) {
Expand All @@ -21,13 +22,14 @@ func TestLoadDeployedAddresses(t *testing.T) {
v090 := semver.MustParse("0.9.0")

bypasserV100 := common.HexToAddress("0x00000000000000000000000000000000000000b1")
bypasserLegacy := common.HexToAddress("0x00000000000000000000000000000000000000b2")
bypasserOld := common.HexToAddress("0x00000000000000000000000000000000000000b3")
proposerV100 := common.HexToAddress("0x00000000000000000000000000000000000000c1")

t.Run("nil datastore", func(t *testing.T) {
t.Parallel()
require.Equal(t, deployedAddresses{}, loadDeployedAddresses(nil, selector, ""))
addrs, err := loadDeployedAddresses(nil, selector, "")
require.NoError(t, err)
require.Equal(t, deployedAddresses{}, addrs)
})

t.Run("matches v1.0.0", func(t *testing.T) {
Expand All @@ -41,7 +43,8 @@ func TestLoadDeployedAddresses(t *testing.T) {
Version: &v100,
}))

addrs := loadDeployedAddresses(ds.Seal(), selector, "")
addrs, err := loadDeployedAddresses(ds.Seal(), selector, "")
require.NoError(t, err)
require.Equal(t, bypasserV100, addrs.Bypasser)
})

Expand All @@ -56,63 +59,50 @@ func TestLoadDeployedAddresses(t *testing.T) {
Version: v090,
}))

addrs := loadDeployedAddresses(ds.Seal(), selector, "")
addrs, err := loadDeployedAddresses(ds.Seal(), selector, "")
require.NoError(t, err)
require.Equal(t, common.Address{}, addrs.Bypasser)
})

t.Run("falls back to legacy nil version", func(t *testing.T) {
t.Run("respects qualifier", func(t *testing.T) {
t.Parallel()

store := fakeAddressRefStore{refs: []cldfdatastore.AddressRef{{
ds := cldfdatastore.NewMemoryDataStore()
require.NoError(t, ds.Addresses().Add(cldfdatastore.AddressRef{
ChainSelector: selector,
Address: bypasserLegacy.Hex(),
Type: cldfdatastore.ContractType(mcmscontracts.BypasserManyChainMultisig),
}}}
Address: proposerV100.Hex(),
Type: cldfdatastore.ContractType(mcmscontracts.ProposerManyChainMultisig),
Version: &v100,
Qualifier: "prod",
}))

addrs, err := loadDeployedAddresses(ds.Seal(), selector, "")
require.NoError(t, err)
require.Equal(t, common.Address{}, addrs.Proposer)

got, ok := findDeployedAddress(store, selector, mcmscontracts.BypasserManyChainMultisig, "")
require.True(t, ok)
require.Equal(t, bypasserLegacy, got)
addrs, err = loadDeployedAddresses(ds.Seal(), selector, "prod")
require.NoError(t, err)
require.Equal(t, proposerV100, addrs.Proposer)
})

t.Run("prefers v1.0.0 over legacy nil version", func(t *testing.T) {
t.Run("duplicate refs", func(t *testing.T) {
t.Parallel()

store := fakeAddressRefStore{refs: []cldfdatastore.AddressRef{
_, err := loadDeployedAddresses(datastoretest.NewDataStore([]cldfdatastore.AddressRef{
{
ChainSelector: selector,
Address: bypasserLegacy.Hex(),
Address: bypasserV100.Hex(),
Type: cldfdatastore.ContractType(mcmscontracts.BypasserManyChainMultisig),
Version: &v100,
},
{
ChainSelector: selector,
Address: bypasserV100.Hex(),
Address: bypasserOld.Hex(),
Type: cldfdatastore.ContractType(mcmscontracts.BypasserManyChainMultisig),
Version: &v100,
},
}}

got, ok := findDeployedAddress(store, selector, mcmscontracts.BypasserManyChainMultisig, "")
require.True(t, ok)
require.Equal(t, bypasserV100, got)
})

t.Run("respects qualifier", func(t *testing.T) {
t.Parallel()

ds := cldfdatastore.NewMemoryDataStore()
require.NoError(t, ds.Addresses().Add(cldfdatastore.AddressRef{
ChainSelector: selector,
Address: proposerV100.Hex(),
Type: cldfdatastore.ContractType(mcmscontracts.ProposerManyChainMultisig),
Version: &v100,
Qualifier: "prod",
}))

addrs := loadDeployedAddresses(ds.Seal(), selector, "")
require.Equal(t, common.Address{}, addrs.Proposer)

addrs = loadDeployedAddresses(ds.Seal(), selector, "prod")
require.Equal(t, proposerV100, addrs.Proposer)
}), selector, "")
require.ErrorIs(t, err, cldfdatastore.ErrAddressRefQueryAmbiguous)
})
}

Expand All @@ -131,34 +121,8 @@ func TestFindDeployedAddress(t *testing.T) {
Version: &v100,
}))

got, ok := findDeployedAddress(ds.Addresses(), selector, mcmscontracts.RBACTimelock, "")
got, ok, err := findDeployedAddress(ds.Addresses(), selector, mcmscontracts.RBACTimelock, "")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, addr, got)
}

type fakeAddressRefStore struct {
refs []cldfdatastore.AddressRef
}

func (f fakeAddressRefStore) Fetch() ([]cldfdatastore.AddressRef, error) {
return f.refs, nil
}

func (f fakeAddressRefStore) Get(key cldfdatastore.AddressRefKey) (cldfdatastore.AddressRef, error) {
for _, ref := range f.refs {
if ref.Key().Equals(key) {
return ref, nil
}
}

return cldfdatastore.AddressRef{}, cldfdatastore.ErrAddressRefNotFound
}

func (f fakeAddressRefStore) Filter(filters ...cldfdatastore.FilterFunc[cldfdatastore.AddressRefKey, cldfdatastore.AddressRef]) []cldfdatastore.AddressRef {
refs := f.refs
for _, filter := range filters {
refs = filter(refs)
}

return refs
}
6 changes: 4 additions & 2 deletions mcms/evm/deploy/sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ func deployMCMSWithTimelock(

qualifier := qualifierFromConfig(in.Config.Qualifier)

existing := loadDeployedAddresses(deps.DataStore, in.ChainSelector, qualifier)
existing, err := loadDeployedAddresses(deps.DataStore, in.ChainSelector, qualifier)
if err != nil {
return sequenceutils.OnChainOutput{}, fmt.Errorf("load deployed addresses: %w", err)
}

d := &deployer{b: b, chain: chain, config: in.Config, qualifier: qualifier}

var err error
if existing.Bypasser, err = d.deployMCMIfNeeded(mcmscontracts.BypasserManyChainMultisig, in.Config.Bypasser, existing.Bypasser); err != nil {
return d.out, err
}
Expand Down
Loading
Loading