Skip to content

Commit 61ad06b

Browse files
feat(mcms): add Solana deploy changeset for MCMS with timelock [CLD-2719]
Register Solana in the MCMS deploy registry with an operations-based sequence that deploys programs, initializes accounts/instances, and grants timelock roles, matching the EVM deploy pattern.
1 parent 2da6931 commit 61ad06b

11 files changed

Lines changed: 1460 additions & 64 deletions

File tree

mcms/evm/deploy/addresses.go

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
package evmdeploy
44

55
import (
6+
"fmt"
7+
68
"github.com/ethereum/go-ethereum/common"
79
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
810
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
@@ -21,9 +23,9 @@ type deployedAddresses struct {
2123
CallProxy common.Address
2224
}
2325

24-
func loadDeployedAddresses(ds cldfdatastore.DataStore, chainSelector uint64, qualifier string) deployedAddresses {
26+
func loadDeployedAddresses(ds cldfdatastore.DataStore, chainSelector uint64, qualifier string) (deployedAddresses, error) {
2527
if ds == nil {
26-
return deployedAddresses{}
28+
return deployedAddresses{}, nil
2729
}
2830

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

4345
for _, l := range lookups {
44-
if addr, ok := findDeployedAddress(ds.Addresses(), chainSelector, l.contractType, qualifier); ok {
46+
addr, ok, err := findDeployedAddress(ds.Addresses(), chainSelector, l.contractType, qualifier)
47+
if err != nil {
48+
return deployedAddresses{}, err
49+
}
50+
if ok {
4551
*l.dest = addr
4652
}
4753
}
4854

49-
return addrs
55+
return addrs, nil
5056
}
5157

5258
// findDeployedAddress returns a previously deployed contract address for this
53-
// deploy package's version. Legacy datastore entries without a version are
54-
// accepted as a fallback; refs with a different version are ignored so a
55-
// version bump can redeploy.
59+
// deploy package's version. Qualifier is always matched exactly, including "".
60+
// Returns ok=false when no ref matches; an error when multiple refs match.
5661
func findDeployedAddress(
5762
store cldfdatastore.AddressRefStore,
5863
chainSelector uint64,
5964
contractType cldf.ContractType,
6065
qualifier string,
61-
) (common.Address, bool) {
62-
baseFilters := make([]cldfdatastore.FilterFunc[cldfdatastore.AddressRefKey, cldfdatastore.AddressRef], 0, 3)
63-
baseFilters = append(baseFilters,
66+
) (common.Address, bool, error) {
67+
version := semvers.V1_0_0
68+
refs := store.Filter(
6469
cldfdatastore.AddressRefByChainSelector(chainSelector),
6570
cldfdatastore.AddressRefByType(cldfdatastore.ContractType(contractType)),
6671
cldfdatastore.AddressRefByQualifier(qualifier),
72+
cldfdatastore.AddressRefByVersion(&version),
6773
)
68-
69-
version := semvers.V1_0_0
70-
refs := store.Filter(append(baseFilters, cldfdatastore.AddressRefByVersion(&version))...)
71-
if len(refs) > 0 {
72-
return common.HexToAddress(refs[0].Address), true
74+
switch len(refs) {
75+
case 0:
76+
return common.Address{}, false, nil
77+
case 1:
78+
return common.HexToAddress(refs[0].Address), true, nil
79+
default:
80+
return common.Address{}, false, fmt.Errorf(
81+
"%w: chain selector %d contract type %s qualifier %q version %s: found %d refs",
82+
cldfdatastore.ErrAddressRefQueryAmbiguous,
83+
chainSelector, contractType, qualifier, version, len(refs),
84+
)
7385
}
74-
75-
refs = store.Filter(baseFilters...)
76-
for _, ref := range refs {
77-
if ref.Version == nil {
78-
return common.HexToAddress(ref.Address), true
79-
}
80-
}
81-
82-
return common.Address{}, false
8386
}
8487

8588
// addressRefWithLabel attaches an optional label to a deployed contract address ref.

mcms/evm/deploy/addresses_test.go

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ func TestLoadDeployedAddresses(t *testing.T) {
2121
v090 := semver.MustParse("0.9.0")
2222

2323
bypasserV100 := common.HexToAddress("0x00000000000000000000000000000000000000b1")
24-
bypasserLegacy := common.HexToAddress("0x00000000000000000000000000000000000000b2")
2524
bypasserOld := common.HexToAddress("0x00000000000000000000000000000000000000b3")
2625
proposerV100 := common.HexToAddress("0x00000000000000000000000000000000000000c1")
2726

2827
t.Run("nil datastore", func(t *testing.T) {
2928
t.Parallel()
30-
require.Equal(t, deployedAddresses{}, loadDeployedAddresses(nil, selector, ""))
29+
addrs, err := loadDeployedAddresses(nil, selector, "")
30+
require.NoError(t, err)
31+
require.Equal(t, deployedAddresses{}, addrs)
3132
})
3233

3334
t.Run("matches v1.0.0", func(t *testing.T) {
@@ -41,7 +42,8 @@ func TestLoadDeployedAddresses(t *testing.T) {
4142
Version: &v100,
4243
}))
4344

44-
addrs := loadDeployedAddresses(ds.Seal(), selector, "")
45+
addrs, err := loadDeployedAddresses(ds.Seal(), selector, "")
46+
require.NoError(t, err)
4547
require.Equal(t, bypasserV100, addrs.Bypasser)
4648
})
4749

@@ -56,63 +58,52 @@ func TestLoadDeployedAddresses(t *testing.T) {
5658
Version: v090,
5759
}))
5860

59-
addrs := loadDeployedAddresses(ds.Seal(), selector, "")
61+
addrs, err := loadDeployedAddresses(ds.Seal(), selector, "")
62+
require.NoError(t, err)
6063
require.Equal(t, common.Address{}, addrs.Bypasser)
6164
})
6265

63-
t.Run("falls back to legacy nil version", func(t *testing.T) {
66+
t.Run("respects qualifier", func(t *testing.T) {
6467
t.Parallel()
6568

66-
store := fakeAddressRefStore{refs: []cldfdatastore.AddressRef{{
69+
ds := cldfdatastore.NewMemoryDataStore()
70+
require.NoError(t, ds.Addresses().Add(cldfdatastore.AddressRef{
6771
ChainSelector: selector,
68-
Address: bypasserLegacy.Hex(),
69-
Type: cldfdatastore.ContractType(mcmscontracts.BypasserManyChainMultisig),
70-
}}}
72+
Address: proposerV100.Hex(),
73+
Type: cldfdatastore.ContractType(mcmscontracts.ProposerManyChainMultisig),
74+
Version: &v100,
75+
Qualifier: "prod",
76+
}))
7177

72-
got, ok := findDeployedAddress(store, selector, mcmscontracts.BypasserManyChainMultisig, "")
73-
require.True(t, ok)
74-
require.Equal(t, bypasserLegacy, got)
78+
addrs, err := loadDeployedAddresses(ds.Seal(), selector, "")
79+
require.NoError(t, err)
80+
require.Equal(t, common.Address{}, addrs.Proposer)
81+
82+
addrs, err = loadDeployedAddresses(ds.Seal(), selector, "prod")
83+
require.NoError(t, err)
84+
require.Equal(t, proposerV100, addrs.Proposer)
7585
})
7686

77-
t.Run("prefers v1.0.0 over legacy nil version", func(t *testing.T) {
87+
t.Run("duplicate refs", func(t *testing.T) {
7888
t.Parallel()
7989

8090
store := fakeAddressRefStore{refs: []cldfdatastore.AddressRef{
8191
{
8292
ChainSelector: selector,
83-
Address: bypasserLegacy.Hex(),
93+
Address: bypasserV100.Hex(),
8494
Type: cldfdatastore.ContractType(mcmscontracts.BypasserManyChainMultisig),
95+
Version: &v100,
8596
},
8697
{
8798
ChainSelector: selector,
88-
Address: bypasserV100.Hex(),
99+
Address: bypasserOld.Hex(),
89100
Type: cldfdatastore.ContractType(mcmscontracts.BypasserManyChainMultisig),
90101
Version: &v100,
91102
},
92103
}}
93104

94-
got, ok := findDeployedAddress(store, selector, mcmscontracts.BypasserManyChainMultisig, "")
95-
require.True(t, ok)
96-
require.Equal(t, bypasserV100, got)
97-
})
98-
99-
t.Run("respects qualifier", func(t *testing.T) {
100-
t.Parallel()
101-
102-
ds := cldfdatastore.NewMemoryDataStore()
103-
require.NoError(t, ds.Addresses().Add(cldfdatastore.AddressRef{
104-
ChainSelector: selector,
105-
Address: proposerV100.Hex(),
106-
Type: cldfdatastore.ContractType(mcmscontracts.ProposerManyChainMultisig),
107-
Version: &v100,
108-
Qualifier: "prod",
109-
}))
110-
111-
addrs := loadDeployedAddresses(ds.Seal(), selector, "")
112-
require.Equal(t, common.Address{}, addrs.Proposer)
113-
114-
addrs = loadDeployedAddresses(ds.Seal(), selector, "prod")
115-
require.Equal(t, proposerV100, addrs.Proposer)
105+
_, err := loadDeployedAddresses(fakeDataStore{store: store}, selector, "")
106+
require.ErrorIs(t, err, cldfdatastore.ErrAddressRefQueryAmbiguous)
116107
})
117108
}
118109

@@ -131,7 +122,8 @@ func TestFindDeployedAddress(t *testing.T) {
131122
Version: &v100,
132123
}))
133124

134-
got, ok := findDeployedAddress(ds.Addresses(), selector, mcmscontracts.RBACTimelock, "")
125+
got, ok, err := findDeployedAddress(ds.Addresses(), selector, mcmscontracts.RBACTimelock, "")
126+
require.NoError(t, err)
135127
require.True(t, ok)
136128
require.Equal(t, addr, got)
137129
}
@@ -162,3 +154,15 @@ func (f fakeAddressRefStore) Filter(filters ...cldfdatastore.FilterFunc[cldfdata
162154

163155
return refs
164156
}
157+
158+
type fakeDataStore struct {
159+
store fakeAddressRefStore
160+
}
161+
162+
func (f fakeDataStore) Addresses() cldfdatastore.AddressRefStore { return f.store }
163+
164+
func (f fakeDataStore) ChainMetadata() cldfdatastore.ChainMetadataStore { return nil }
165+
166+
func (f fakeDataStore) ContractMetadata() cldfdatastore.ContractMetadataStore { return nil }
167+
168+
func (f fakeDataStore) EnvMetadata() cldfdatastore.EnvMetadataStore { return nil }

mcms/evm/deploy/sequence.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ func deployMCMSWithTimelock(
5454

5555
qualifier := qualifierFromConfig(in.Config.Qualifier)
5656

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

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

61-
var err error
6264
if existing.Bypasser, err = d.deployMCMIfNeeded(mcmscontracts.BypasserManyChainMultisig, in.Config.Bypasser, existing.Bypasser); err != nil {
6365
return d.out, err
6466
}

mcms/solana/deploy/addresses.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package soldeploy
2+
3+
import (
4+
"fmt"
5+
6+
solanago "github.com/gagliardetto/solana-go"
7+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
8+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
9+
mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms"
10+
11+
"github.com/smartcontractkit/cld-changesets/internal/semvers"
12+
legacysolana "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana"
13+
)
14+
15+
// deployedAddresses holds the on-chain state of an MCMS+timelock deployment on one
16+
// Solana chain. Zero values mean the corresponding program or account has not yet
17+
// been deployed/initialized.
18+
type deployedAddresses struct {
19+
AccessControllerProgram solanago.PublicKey
20+
ProposerAccessControllerAccount solanago.PublicKey
21+
ExecutorAccessControllerAccount solanago.PublicKey
22+
CancellerAccessControllerAccount solanago.PublicKey
23+
BypasserAccessControllerAccount solanago.PublicKey
24+
McmProgram solanago.PublicKey
25+
ProposerMCMSeed legacysolana.PDASeed
26+
CancellerMCMSeed legacysolana.PDASeed
27+
BypasserMCMSeed legacysolana.PDASeed
28+
TimelockProgram solanago.PublicKey
29+
TimelockSeed legacysolana.PDASeed
30+
}
31+
32+
func (d deployedAddresses) hasProposerMCM() bool {
33+
return d.ProposerMCMSeed != (legacysolana.PDASeed{})
34+
}
35+
func (d deployedAddresses) hasCancellerMCM() bool {
36+
return d.CancellerMCMSeed != (legacysolana.PDASeed{})
37+
}
38+
func (d deployedAddresses) hasBypasserMCM() bool {
39+
return d.BypasserMCMSeed != (legacysolana.PDASeed{})
40+
}
41+
func (d deployedAddresses) hasTimelock() bool { return d.TimelockSeed != (legacysolana.PDASeed{}) }
42+
43+
// loadDeployedAddresses returns the current deployment state for the given chain
44+
// and qualifier by reading address refs from the datastore. A zero value in any
45+
// field means the corresponding program or account has not been deployed yet.
46+
func loadDeployedAddresses(ds cldfdatastore.DataStore, chainSelector uint64, qualifier string) (deployedAddresses, error) {
47+
if ds == nil {
48+
return deployedAddresses{}, nil
49+
}
50+
51+
var addrs deployedAddresses
52+
53+
// findRef returns the address string for a given contract type at this
54+
// deploy package's version. Qualifier is always matched exactly, including "".
55+
// Returns ("", nil) when no ref matches; an error when multiple refs match.
56+
findRef := func(ct cldf.ContractType) (string, error) {
57+
version := semvers.V1_0_0
58+
refs := ds.Addresses().Filter(
59+
cldfdatastore.AddressRefByChainSelector(chainSelector),
60+
cldfdatastore.AddressRefByType(cldfdatastore.ContractType(ct)),
61+
cldfdatastore.AddressRefByQualifier(qualifier),
62+
cldfdatastore.AddressRefByVersion(&version),
63+
)
64+
switch len(refs) {
65+
case 0:
66+
return "", nil
67+
case 1:
68+
return refs[0].Address, nil
69+
default:
70+
return "", fmt.Errorf(
71+
"%w: chain selector %d contract type %s qualifier %q version %s: found %d refs",
72+
cldfdatastore.ErrAddressRefQueryAmbiguous,
73+
chainSelector, ct, qualifier, version, len(refs),
74+
)
75+
}
76+
}
77+
78+
loadPubkey := func(ct cldf.ContractType, dest *solanago.PublicKey) error {
79+
addr, err := findRef(ct)
80+
if err != nil {
81+
return err
82+
}
83+
if addr == "" {
84+
return nil
85+
}
86+
pk, err := solanago.PublicKeyFromBase58(addr)
87+
if err != nil {
88+
return fmt.Errorf("parse %s address %q: %w", ct, addr, err)
89+
}
90+
*dest = pk
91+
92+
return nil
93+
}
94+
95+
// Plain base58 addresses (program IDs and AC accounts)
96+
for _, entry := range []struct {
97+
ct cldf.ContractType
98+
dest *solanago.PublicKey
99+
}{
100+
{mcmscontracts.AccessControllerProgram, &addrs.AccessControllerProgram},
101+
{mcmscontracts.ProposerAccessControllerAccount, &addrs.ProposerAccessControllerAccount},
102+
{mcmscontracts.ExecutorAccessControllerAccount, &addrs.ExecutorAccessControllerAccount},
103+
{mcmscontracts.CancellerAccessControllerAccount, &addrs.CancellerAccessControllerAccount},
104+
{mcmscontracts.BypasserAccessControllerAccount, &addrs.BypasserAccessControllerAccount},
105+
{mcmscontracts.ManyChainMultisigProgram, &addrs.McmProgram},
106+
{mcmscontracts.RBACTimelockProgram, &addrs.TimelockProgram},
107+
} {
108+
if err := loadPubkey(entry.ct, entry.dest); err != nil {
109+
return deployedAddresses{}, err
110+
}
111+
}
112+
113+
// Seed-encoded MCM instance addresses (programID:seed)
114+
for ct, dst := range map[cldf.ContractType]*legacysolana.PDASeed{
115+
mcmscontracts.ProposerManyChainMultisig: &addrs.ProposerMCMSeed,
116+
mcmscontracts.CancellerManyChainMultisig: &addrs.CancellerMCMSeed,
117+
mcmscontracts.BypasserManyChainMultisig: &addrs.BypasserMCMSeed,
118+
} {
119+
addr, err := findRef(ct)
120+
if err != nil {
121+
return deployedAddresses{}, err
122+
}
123+
if addr == "" {
124+
continue
125+
}
126+
programID, seed, err := legacysolana.DecodeAddressWithSeed(addr)
127+
if err != nil {
128+
return deployedAddresses{}, fmt.Errorf("decode %s address %q: %w", ct, addr, err)
129+
}
130+
// The program ID embedded in instance refs is authoritative when the
131+
// program-level ref is absent.
132+
if addrs.McmProgram.IsZero() {
133+
addrs.McmProgram = programID
134+
}
135+
*dst = seed
136+
}
137+
138+
// Seed-encoded timelock instance (programID:seed)
139+
if addr, err := findRef(mcmscontracts.RBACTimelock); err != nil {
140+
return deployedAddresses{}, err
141+
} else if addr != "" {
142+
programID, seed, err := legacysolana.DecodeAddressWithSeed(addr)
143+
if err != nil {
144+
return deployedAddresses{}, fmt.Errorf("decode %s address %q: %w", mcmscontracts.RBACTimelock, addr, err)
145+
}
146+
if addrs.TimelockProgram.IsZero() {
147+
addrs.TimelockProgram = programID
148+
}
149+
addrs.TimelockSeed = seed
150+
}
151+
152+
return addrs, nil
153+
}

0 commit comments

Comments
 (0)