Skip to content

Commit f79f36c

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 f79f36c

8 files changed

Lines changed: 1322 additions & 0 deletions

File tree

mcms/solana/deploy/addresses.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package soldeploy
2+
3+
import (
4+
solanago "github.com/gagliardetto/solana-go"
5+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
6+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
7+
mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms"
8+
9+
"github.com/smartcontractkit/cld-changesets/internal/semvers"
10+
legacysolana "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana"
11+
)
12+
13+
// deployedAddresses holds the on-chain state of an MCMS+timelock deployment on one
14+
// Solana chain. Zero values mean the corresponding program or account has not yet
15+
// been deployed/initialized.
16+
type deployedAddresses struct {
17+
AccessControllerProgram solanago.PublicKey
18+
ProposerAccessControllerAccount solanago.PublicKey
19+
ExecutorAccessControllerAccount solanago.PublicKey
20+
CancellerAccessControllerAccount solanago.PublicKey
21+
BypasserAccessControllerAccount solanago.PublicKey
22+
McmProgram solanago.PublicKey
23+
ProposerMCMSeed legacysolana.PDASeed
24+
CancellerMCMSeed legacysolana.PDASeed
25+
BypasserMCMSeed legacysolana.PDASeed
26+
TimelockProgram solanago.PublicKey
27+
TimelockSeed legacysolana.PDASeed
28+
}
29+
30+
func (d deployedAddresses) hasProposerMCM() bool {
31+
return d.ProposerMCMSeed != (legacysolana.PDASeed{})
32+
}
33+
func (d deployedAddresses) hasCancellerMCM() bool {
34+
return d.CancellerMCMSeed != (legacysolana.PDASeed{})
35+
}
36+
func (d deployedAddresses) hasBypasserMCM() bool {
37+
return d.BypasserMCMSeed != (legacysolana.PDASeed{})
38+
}
39+
func (d deployedAddresses) hasTimelock() bool { return d.TimelockSeed != (legacysolana.PDASeed{}) }
40+
41+
// loadDeployedAddresses returns the current deployment state for the given chain
42+
// and qualifier by reading address refs from the datastore. A zero value in any
43+
// field means the corresponding program or account has not been deployed yet.
44+
//
45+
// Version filtering matches EVM behaviour: prefer v1.0.0 refs; fall back to
46+
// unversioned refs; ignore refs with a different explicit version so a version
47+
// bump can trigger a re-deploy.
48+
func loadDeployedAddresses(ds cldfdatastore.DataStore, chainSelector uint64, qualifier string) deployedAddresses {
49+
if ds == nil {
50+
return deployedAddresses{}
51+
}
52+
53+
var addrs deployedAddresses
54+
55+
// findRef returns the address string for a given contract type, applying
56+
// qualifier and version filtering. Returns ("", false) if not found.
57+
findRef := func(ct cldf.ContractType) (string, bool) {
58+
base := make([]cldfdatastore.FilterFunc[cldfdatastore.AddressRefKey, cldfdatastore.AddressRef], 0, 4)
59+
base = append(base,
60+
cldfdatastore.AddressRefByChainSelector(chainSelector),
61+
cldfdatastore.AddressRefByType(cldfdatastore.ContractType(ct)),
62+
cldfdatastore.AddressRefByQualifier(qualifier),
63+
)
64+
65+
v := semvers.V1_0_0
66+
if refs := ds.Addresses().Filter(append(base, cldfdatastore.AddressRefByVersion(&v))...); len(refs) > 0 {
67+
return refs[0].Address, true
68+
}
69+
70+
for _, ref := range ds.Addresses().Filter(base...) {
71+
if ref.Version == nil {
72+
return ref.Address, true
73+
}
74+
}
75+
76+
return "", false
77+
}
78+
79+
// Plain base58 addresses (program IDs and AC accounts)
80+
if addr, ok := findRef(mcmscontracts.AccessControllerProgram); ok {
81+
addrs.AccessControllerProgram, _ = solanago.PublicKeyFromBase58(addr)
82+
}
83+
if addr, ok := findRef(mcmscontracts.ProposerAccessControllerAccount); ok {
84+
addrs.ProposerAccessControllerAccount, _ = solanago.PublicKeyFromBase58(addr)
85+
}
86+
if addr, ok := findRef(mcmscontracts.ExecutorAccessControllerAccount); ok {
87+
addrs.ExecutorAccessControllerAccount, _ = solanago.PublicKeyFromBase58(addr)
88+
}
89+
if addr, ok := findRef(mcmscontracts.CancellerAccessControllerAccount); ok {
90+
addrs.CancellerAccessControllerAccount, _ = solanago.PublicKeyFromBase58(addr)
91+
}
92+
if addr, ok := findRef(mcmscontracts.BypasserAccessControllerAccount); ok {
93+
addrs.BypasserAccessControllerAccount, _ = solanago.PublicKeyFromBase58(addr)
94+
}
95+
96+
// MCM program — prefer ManyChainMultisigProgram; fall back to reading
97+
// from encoded instance refs if only instances were saved.
98+
if addr, ok := findRef(mcmscontracts.ManyChainMultisigProgram); ok {
99+
addrs.McmProgram, _ = solanago.PublicKeyFromBase58(addr)
100+
}
101+
102+
// Seed-encoded MCM instance addresses (programID:seed)
103+
for ct, dst := range map[cldf.ContractType]*legacysolana.PDASeed{
104+
mcmscontracts.ProposerManyChainMultisig: &addrs.ProposerMCMSeed,
105+
mcmscontracts.CancellerManyChainMultisig: &addrs.CancellerMCMSeed,
106+
mcmscontracts.BypasserManyChainMultisig: &addrs.BypasserMCMSeed,
107+
} {
108+
addr, ok := findRef(ct)
109+
if !ok {
110+
continue
111+
}
112+
programID, seed, err := legacysolana.DecodeAddressWithSeed(addr)
113+
if err != nil {
114+
continue
115+
}
116+
// The program ID embedded in instance refs is authoritative when the
117+
// program-level ref is absent.
118+
if addrs.McmProgram.IsZero() {
119+
addrs.McmProgram = programID
120+
}
121+
*dst = seed
122+
}
123+
124+
// Timelock program
125+
if addr, ok := findRef(mcmscontracts.RBACTimelockProgram); ok {
126+
addrs.TimelockProgram, _ = solanago.PublicKeyFromBase58(addr)
127+
}
128+
129+
// Seed-encoded timelock instance (programID:seed)
130+
if addr, ok := findRef(mcmscontracts.RBACTimelock); ok {
131+
programID, seed, err := legacysolana.DecodeAddressWithSeed(addr)
132+
if err == nil {
133+
if addrs.TimelockProgram.IsZero() {
134+
addrs.TimelockProgram = programID
135+
}
136+
addrs.TimelockSeed = seed
137+
}
138+
}
139+
140+
return addrs
141+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package soldeploy
2+
3+
import (
4+
"testing"
5+
6+
"github.com/Masterminds/semver/v3"
7+
solanago "github.com/gagliardetto/solana-go"
8+
chainselectors "github.com/smartcontractkit/chain-selectors"
9+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
10+
mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/smartcontractkit/cld-changesets/internal/semvers"
14+
legacysolana "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana"
15+
solutils "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/solutils"
16+
)
17+
18+
func TestLoadDeployedAddresses(t *testing.T) {
19+
t.Parallel()
20+
21+
selector := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
22+
v100 := semvers.V1_0_0
23+
v090 := semver.MustParse("0.9.0")
24+
25+
acProgram := solanago.MustPublicKeyFromBase58(solutils.GetProgramID(solutils.ProgAccessController))
26+
mcmProgram := solanago.MustPublicKeyFromBase58(solutils.GetProgramID(solutils.ProgMCM))
27+
proposerAC := solanago.MustPublicKeyFromBase58("11111111111111111111111111111112")
28+
var proposerSeed legacysolana.PDASeed
29+
copy(proposerSeed[:], "proposer-seed-123456789012345678") // 32 bytes
30+
proposerMCM := legacysolana.EncodeAddressWithSeed(mcmProgram, proposerSeed)
31+
32+
t.Run("nil datastore", func(t *testing.T) {
33+
t.Parallel()
34+
require.Equal(t, deployedAddresses{}, loadDeployedAddresses(nil, selector, ""))
35+
})
36+
37+
t.Run("matches v1.0.0 program", func(t *testing.T) {
38+
t.Parallel()
39+
40+
ds := cldfdatastore.NewMemoryDataStore()
41+
require.NoError(t, ds.Addresses().Add(cldfdatastore.AddressRef{
42+
ChainSelector: selector,
43+
Address: acProgram.String(),
44+
Type: cldfdatastore.ContractType(mcmscontracts.AccessControllerProgram),
45+
Version: &v100,
46+
}))
47+
48+
addrs := loadDeployedAddresses(ds.Seal(), selector, "")
49+
require.Equal(t, acProgram, addrs.AccessControllerProgram)
50+
})
51+
52+
t.Run("ignores older version", func(t *testing.T) {
53+
t.Parallel()
54+
55+
ds := cldfdatastore.NewMemoryDataStore()
56+
require.NoError(t, ds.Addresses().Add(cldfdatastore.AddressRef{
57+
ChainSelector: selector,
58+
Address: acProgram.String(),
59+
Type: cldfdatastore.ContractType(mcmscontracts.AccessControllerProgram),
60+
Version: v090,
61+
}))
62+
63+
addrs := loadDeployedAddresses(ds.Seal(), selector, "")
64+
require.True(t, addrs.AccessControllerProgram.IsZero())
65+
})
66+
67+
t.Run("falls back to legacy nil version", func(t *testing.T) {
68+
t.Parallel()
69+
70+
store := fakeAddressRefStore{refs: []cldfdatastore.AddressRef{{
71+
ChainSelector: selector,
72+
Address: proposerAC.String(),
73+
Type: cldfdatastore.ContractType(mcmscontracts.ProposerAccessControllerAccount),
74+
}}}
75+
76+
addrs := loadDeployedAddresses(fakeDataStore{store: store}, selector, "")
77+
require.Equal(t, proposerAC, addrs.ProposerAccessControllerAccount)
78+
})
79+
80+
t.Run("respects qualifier", func(t *testing.T) {
81+
t.Parallel()
82+
83+
ds := cldfdatastore.NewMemoryDataStore()
84+
require.NoError(t, ds.Addresses().Add(cldfdatastore.AddressRef{
85+
ChainSelector: selector,
86+
Address: proposerAC.String(),
87+
Type: cldfdatastore.ContractType(mcmscontracts.ProposerAccessControllerAccount),
88+
Version: &v100,
89+
Qualifier: "prod",
90+
}))
91+
92+
addrs := loadDeployedAddresses(ds.Seal(), selector, "")
93+
require.True(t, addrs.ProposerAccessControllerAccount.IsZero())
94+
95+
addrs = loadDeployedAddresses(ds.Seal(), selector, "prod")
96+
require.Equal(t, proposerAC, addrs.ProposerAccessControllerAccount)
97+
})
98+
99+
t.Run("loads MCM instance seed and program from encoded address", 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: proposerMCM,
106+
Type: cldfdatastore.ContractType(mcmscontracts.ProposerManyChainMultisig),
107+
Version: &v100,
108+
}))
109+
110+
addrs := loadDeployedAddresses(ds.Seal(), selector, "")
111+
require.Equal(t, mcmProgram, addrs.McmProgram)
112+
require.Equal(t, proposerSeed, addrs.ProposerMCMSeed)
113+
require.True(t, addrs.hasProposerMCM())
114+
})
115+
}
116+
117+
type fakeAddressRefStore struct {
118+
refs []cldfdatastore.AddressRef
119+
}
120+
121+
func (f fakeAddressRefStore) Fetch() ([]cldfdatastore.AddressRef, error) {
122+
return f.refs, nil
123+
}
124+
125+
func (f fakeAddressRefStore) Get(key cldfdatastore.AddressRefKey) (cldfdatastore.AddressRef, error) {
126+
for _, ref := range f.refs {
127+
if ref.Key().Equals(key) {
128+
return ref, nil
129+
}
130+
}
131+
132+
return cldfdatastore.AddressRef{}, cldfdatastore.ErrAddressRefNotFound
133+
}
134+
135+
func (f fakeAddressRefStore) Filter(filters ...cldfdatastore.FilterFunc[cldfdatastore.AddressRefKey, cldfdatastore.AddressRef]) []cldfdatastore.AddressRef {
136+
refs := f.refs
137+
for _, filter := range filters {
138+
refs = filter(refs)
139+
}
140+
141+
return refs
142+
}
143+
144+
type fakeDataStore struct {
145+
store fakeAddressRefStore
146+
}
147+
148+
func (f fakeDataStore) Addresses() cldfdatastore.AddressRefStore { return f.store }
149+
150+
func (f fakeDataStore) ChainMetadata() cldfdatastore.ChainMetadataStore { return nil }
151+
152+
func (f fakeDataStore) ContractMetadata() cldfdatastore.ContractMetadataStore { return nil }
153+
154+
func (f fakeDataStore) EnvMetadata() cldfdatastore.EnvMetadataStore { return nil }

0 commit comments

Comments
 (0)