Skip to content

Commit 9293d0b

Browse files
fix: add solana support for transfer changeset
Looks like solana was missed for the transfer to timelock changeset as it only support evm currently JIRA: https://smartcontract-it.atlassian.net/browse/CLD-2762
1 parent f75c78b commit 9293d0b

10 files changed

Lines changed: 1093 additions & 0 deletions

File tree

mcms/changesets/transfer-to-timelock/all/wire.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ package all
44
import (
55
_ "github.com/smartcontractkit/cld-changesets/mcms/evm/readers"
66
_ "github.com/smartcontractkit/cld-changesets/mcms/evm/transfer-to-timelock"
7+
_ "github.com/smartcontractkit/cld-changesets/mcms/solana/readers"
8+
_ "github.com/smartcontractkit/cld-changesets/mcms/solana/transfer-to-timelock"
79
)
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package soltransfertotimelock_test
2+
3+
import (
4+
"crypto/ecdsa"
5+
"testing"
6+
"time"
7+
8+
"github.com/gagliardetto/solana-go"
9+
"github.com/stretchr/testify/require"
10+
11+
chainselectors "github.com/smartcontractkit/chain-selectors"
12+
cldfsol "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
13+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
14+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
15+
mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms"
16+
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
17+
cldftesthelpers "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils/testhelpers"
18+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment"
19+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime"
20+
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
21+
mcmstypes "github.com/smartcontractkit/mcms/types"
22+
23+
mcmBindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/v0_1_1/mcm"
24+
25+
"github.com/smartcontractkit/cld-changesets/datastore/refkey"
26+
"github.com/smartcontractkit/cld-changesets/internal/semvers"
27+
"github.com/smartcontractkit/cld-changesets/internal/testutil/solanatest"
28+
solstate "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana"
29+
soltestutils "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/testutils"
30+
mcmsdeploy "github.com/smartcontractkit/cld-changesets/mcms/changesets/deploy"
31+
transfertotimelock "github.com/smartcontractkit/cld-changesets/mcms/changesets/transfer-to-timelock"
32+
pdasol "github.com/smartcontractkit/cld-changesets/pkg/family/solana"
33+
34+
_ "github.com/smartcontractkit/cld-changesets/mcms/solana/deploy"
35+
_ "github.com/smartcontractkit/cld-changesets/mcms/solana/readers"
36+
_ "github.com/smartcontractkit/cld-changesets/mcms/solana/transfer-to-timelock"
37+
)
38+
39+
// TestChangeset shares one Solana container across the transfer and idempotent subtests to
40+
// avoid paying the ~30 s container-startup cost multiple times. OnlyAcceptOwnership keeps its
41+
// own environment because it requires a pending-ownership state that conflicts with the other
42+
// flows.
43+
//
44+
//nolint:paralleltest // global mcm.SetProgramID state; serialized via soltestutils.PreloadMCMS lock
45+
func TestChangeset(t *testing.T) {
46+
selector := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
47+
48+
t.Run("OnlyAcceptOwnership", func(t *testing.T) { //nolint:paralleltest
49+
rt, chain, mcmsState := newTransferToTimelockTestEnv(t, selector)
50+
51+
timelockSignerPDA := pdasol.GetTimelockSignerPDA(mcmsState.TimelockProgram, mcmsState.TimelockSeed)
52+
proposerConfigPDA := pdasol.GetMCMConfigPDA(mcmsState.McmProgram, mcmsState.ProposerMcmSeed)
53+
deployer := chain.DeployerKey.PublicKey()
54+
55+
require.NoError(t, transferProposerMCMOnChain(t, chain, mcmsState, timelockSignerPDA))
56+
57+
assertMCMOwner(t, deployer, proposerConfigPDA, chain)
58+
59+
err := rt.Exec(
60+
runtime.ChangesetTask(transfertotimelock.Changeset{}, acceptOwnershipInput(selector)),
61+
)
62+
require.NoError(t, err)
63+
64+
err = rt.Exec(runtime.SignAndExecuteProposalsTask([]*ecdsa.PrivateKey{cldftesthelpers.TestXXXMCMSSigner}))
65+
require.NoError(t, err)
66+
require.Len(t, rt.State().Proposals, 1)
67+
require.True(t, rt.State().Proposals[0].IsExecuted)
68+
69+
assertMCMOwner(t, timelockSignerPDA, proposerConfigPDA, chain)
70+
})
71+
72+
rt, chain, mcmsState := newTransferToTimelockTestEnv(t, selector)
73+
timelockSignerPDA := pdasol.GetTimelockSignerPDA(mcmsState.TimelockProgram, mcmsState.TimelockSeed)
74+
proposerConfigPDA := pdasol.GetMCMConfigPDA(mcmsState.McmProgram, mcmsState.ProposerMcmSeed)
75+
transferInput := transferToTimelockInput(selector)
76+
77+
t.Run("TransferOwnershipToTimelock", func(t *testing.T) { //nolint:paralleltest
78+
deployer := chain.DeployerKey.PublicKey()
79+
assertMCMOwner(t, deployer, proposerConfigPDA, chain)
80+
81+
execTransferToTimelock(t, rt, transferInput)
82+
require.Len(t, rt.State().Proposals, 1)
83+
require.True(t, rt.State().Proposals[0].IsExecuted)
84+
85+
assertMCMOwner(t, timelockSignerPDA, proposerConfigPDA, chain)
86+
})
87+
88+
t.Run("IdempotentWhenAlreadyOwnedByTimelock", func(t *testing.T) { //nolint:paralleltest
89+
ensureProposerOwnedByTimelock(t, rt, chain, timelockSignerPDA, proposerConfigPDA, transferInput)
90+
91+
taskID, err := runtime.ExecChangeset(rt, transfertotimelock.Changeset{}, transferInput)
92+
require.NoError(t, err)
93+
94+
output, ok := rt.State().Outputs[taskID]
95+
require.True(t, ok)
96+
require.Empty(t, output.MCMSTimelockProposals, "expected no proposal when contract already owned by timelock")
97+
})
98+
}
99+
100+
func transferToTimelockInput(selector uint64) transfertotimelock.Input {
101+
return transfertotimelock.Input{
102+
Cfg: transfertotimelock.Config{
103+
ContractsByChain: map[uint64][]refkey.RefKey{
104+
selector: {contractRef(selector, mcmscontracts.ProposerManyChainMultisig, "")},
105+
},
106+
},
107+
MCMS: &cldf.MCMSTimelockProposalInput{
108+
TimelockAction: mcmstypes.TimelockActionSchedule,
109+
ValidUntil: uint32(time.Now().Add(2 * time.Hour).UTC().Unix()), //nolint:gosec // test timestamp
110+
Description: "Transfer proposer MCM ownership to timelock",
111+
TimelockDelay: mcmstypes.NewDuration(time.Second),
112+
},
113+
}
114+
}
115+
116+
func acceptOwnershipInput(selector uint64) transfertotimelock.Input {
117+
return transfertotimelock.Input{
118+
Cfg: transfertotimelock.Config{
119+
OnlyAcceptOwnership: true,
120+
ContractsByChain: map[uint64][]refkey.RefKey{
121+
selector: {contractRef(selector, mcmscontracts.ProposerManyChainMultisig, "")},
122+
},
123+
},
124+
MCMS: &cldf.MCMSTimelockProposalInput{
125+
TimelockAction: mcmstypes.TimelockActionSchedule,
126+
ValidUntil: uint32(time.Now().Add(2 * time.Hour).UTC().Unix()), //nolint:gosec // test timestamp
127+
Description: "Accept proposer MCM ownership on timelock",
128+
TimelockDelay: mcmstypes.NewDuration(time.Second),
129+
},
130+
}
131+
}
132+
133+
func execTransferToTimelock(t *testing.T, rt *runtime.Runtime, input transfertotimelock.Input) {
134+
t.Helper()
135+
136+
err := rt.Exec(
137+
runtime.ChangesetTask(transfertotimelock.Changeset{}, input),
138+
runtime.SignAndExecuteProposalsTask([]*ecdsa.PrivateKey{cldftesthelpers.TestXXXMCMSSigner}),
139+
)
140+
require.NoError(t, err)
141+
}
142+
143+
func ensureProposerOwnedByTimelock(
144+
t *testing.T,
145+
rt *runtime.Runtime,
146+
chain cldfsol.Chain,
147+
timelockSignerPDA, proposerConfigPDA solana.PublicKey,
148+
input transfertotimelock.Input,
149+
) {
150+
t.Helper()
151+
152+
if mcmOwner(t, proposerConfigPDA, chain) == timelockSignerPDA {
153+
return
154+
}
155+
156+
execTransferToTimelock(t, rt, input)
157+
assertMCMOwner(t, timelockSignerPDA, proposerConfigPDA, chain)
158+
}
159+
160+
func transferProposerMCMOnChain(
161+
t *testing.T,
162+
chain cldfsol.Chain,
163+
mcmsState *solstate.MCMSWithTimelockState,
164+
timelockSignerPDA solana.PublicKey,
165+
) error {
166+
t.Helper()
167+
168+
configPDA := pdasol.GetMCMConfigPDA(mcmsState.McmProgram, mcmsState.ProposerMcmSeed)
169+
mcmBindings.SetProgramID(mcmsState.McmProgram)
170+
171+
ix, err := mcmBindings.NewTransferOwnershipInstruction(
172+
mcmsState.ProposerMcmSeed,
173+
timelockSignerPDA,
174+
configPDA,
175+
chain.DeployerKey.PublicKey(),
176+
).ValidateAndBuild()
177+
if err != nil {
178+
return err
179+
}
180+
181+
return chain.Confirm([]solana.Instruction{&seededInstruction{Instruction: ix, programID: mcmsState.McmProgram}})
182+
}
183+
184+
type seededInstruction struct {
185+
*mcmBindings.Instruction
186+
programID solana.PublicKey
187+
}
188+
189+
func (s *seededInstruction) ProgramID() solana.PublicKey {
190+
return s.programID
191+
}
192+
193+
func newTransferToTimelockTestEnv(
194+
t *testing.T,
195+
selector uint64,
196+
) (*runtime.Runtime, cldfsol.Chain, *solstate.MCMSWithTimelockState) {
197+
t.Helper()
198+
199+
programsPath, programIDs, _ := soltestutils.PreloadMCMS(t, selector)
200+
rt, err := runtime.New(t.Context(), runtime.WithEnvOpts(
201+
environment.WithSolanaContainer(t, []uint64{selector}, programsPath, programIDs),
202+
environment.WithDatastore(solanatest.NewDataStoreWithMCMSPrograms(t, selector)),
203+
environment.WithLogger(logger.Test(t)),
204+
))
205+
require.NoError(t, err)
206+
207+
chain := rt.Environment().BlockChains.SolanaChains()[selector]
208+
209+
err = rt.Exec(
210+
runtime.ChangesetTask(mcmsdeploy.Changeset{}, mcmsdeploy.Input{
211+
ConfigByChain: map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
212+
selector: cldftesthelpers.SingleGroupTimelockConfig(t),
213+
},
214+
}),
215+
)
216+
require.NoError(t, err)
217+
218+
refs := rt.Environment().DataStore.Addresses().Filter(cldfdatastore.AddressRefByChainSelector(selector))
219+
mcmsState, err := solstate.MaybeLoadMCMSWithTimelockChainStateV2(refs)
220+
require.NoError(t, err)
221+
soltestutils.FundSignerPDAs(t, chain, mcmsState)
222+
223+
return rt, chain, mcmsState
224+
}
225+
226+
func contractRef(chainSelector uint64, contractType cldf.ContractType, qualifier string) refkey.RefKey {
227+
return refkey.New(chainSelector, cldfdatastore.ContractType(contractType), &semvers.V1_0_0, qualifier)
228+
}
229+
230+
func mcmOwner(t *testing.T, configPDA solana.PublicKey, chain cldfsol.Chain) solana.PublicKey {
231+
t.Helper()
232+
233+
var config mcmBindings.MultisigConfig
234+
err := chain.GetAccountDataBorshInto(t.Context(), configPDA, &config)
235+
require.NoError(t, err)
236+
237+
return config.Owner
238+
}
239+
240+
func assertMCMOwner(
241+
t *testing.T,
242+
want solana.PublicKey,
243+
configPDA solana.PublicKey,
244+
chain cldfsol.Chain,
245+
) {
246+
t.Helper()
247+
248+
require.Equal(t, want, mcmOwner(t, configPDA, chain))
249+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package soltransfertotimelock
2+
3+
import (
4+
"fmt"
5+
6+
solanago "github.com/gagliardetto/solana-go"
7+
"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/datastore/refkey"
12+
legacysolana "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana"
13+
familysolana "github.com/smartcontractkit/cld-changesets/pkg/family/solana"
14+
)
15+
16+
// OwnableContract identifies a Solana ownable program account to transfer.
17+
type OwnableContract struct {
18+
ProgramID solanago.PublicKey
19+
Seed legacysolana.PDASeed
20+
OwnerPDA solanago.PublicKey
21+
Type cldf.ContractType
22+
}
23+
24+
func resolveOwnableContract(env cldf.Environment, chainSelector uint64, ref refkey.RefKey) (OwnableContract, error) {
25+
if ref.ChainSelector != 0 && ref.ChainSelector != chainSelector {
26+
return OwnableContract{}, fmt.Errorf(
27+
"ref chain selector %d does not match chain %d",
28+
ref.ChainSelector,
29+
chainSelector,
30+
)
31+
}
32+
if ref.ChainSelector == 0 {
33+
ref.ChainSelector = chainSelector
34+
}
35+
36+
resolved, err := ref.Resolve(env)
37+
if err != nil {
38+
return OwnableContract{}, err
39+
}
40+
41+
contractType := cldf.ContractType(resolved.Type)
42+
programID, seed, err := legacysolana.DecodeAddressWithSeed(resolved.Address)
43+
if err != nil {
44+
account, parseErr := solanago.PublicKeyFromBase58(resolved.Address)
45+
if parseErr != nil {
46+
return OwnableContract{}, fmt.Errorf("parse contract address %q: %w", resolved.Address, err)
47+
}
48+
49+
acProgram, acErr := accessControllerProgramFromDatastore(env, chainSelector, ref.Qualifier)
50+
if acErr != nil {
51+
return OwnableContract{}, acErr
52+
}
53+
54+
return OwnableContract{
55+
ProgramID: acProgram,
56+
OwnerPDA: account,
57+
Type: contractType,
58+
}, nil
59+
}
60+
61+
ownerPDA, err := ownerPDAForSeededContract(programID, seed, contractType)
62+
if err != nil {
63+
return OwnableContract{}, err
64+
}
65+
66+
return OwnableContract{
67+
ProgramID: programID,
68+
Seed: seed,
69+
OwnerPDA: ownerPDA,
70+
Type: contractType,
71+
}, nil
72+
}
73+
74+
func ownerPDAForSeededContract(
75+
programID solanago.PublicKey,
76+
seed legacysolana.PDASeed,
77+
contractType cldf.ContractType,
78+
) (solanago.PublicKey, error) {
79+
switch contractType {
80+
case mcmscontracts.ProposerManyChainMultisig,
81+
mcmscontracts.CancellerManyChainMultisig,
82+
mcmscontracts.BypasserManyChainMultisig:
83+
return familysolana.GetMCMConfigPDA(programID, seed), nil
84+
case mcmscontracts.RBACTimelock:
85+
return familysolana.GetTimelockConfigPDA(programID, seed), nil
86+
default:
87+
return solanago.PublicKey{}, fmt.Errorf("unsupported seeded contract type %q for transfer to timelock", contractType)
88+
}
89+
}
90+
91+
func accessControllerProgramFromDatastore(env cldf.Environment, chainSelector uint64, qualifier string) (solanago.PublicKey, error) {
92+
if env.DataStore == nil {
93+
return solanago.PublicKey{}, fmt.Errorf("datastore not available for chain %d", chainSelector)
94+
}
95+
96+
ref, err := datastore.FindUniqueRef(env.DataStore.Addresses(), datastore.AddressRef{
97+
ChainSelector: chainSelector,
98+
Type: datastore.ContractType(mcmscontracts.AccessControllerProgram),
99+
Qualifier: qualifier,
100+
})
101+
if err != nil {
102+
return solanago.PublicKey{}, fmt.Errorf("resolve access controller program for chain %d: %w", chainSelector, err)
103+
}
104+
105+
programID, err := solanago.PublicKeyFromBase58(ref.Address)
106+
if err != nil {
107+
return solanago.PublicKey{}, fmt.Errorf("parse access controller program for chain %d: %w", chainSelector, err)
108+
}
109+
110+
return programID, nil
111+
}

0 commit comments

Comments
 (0)