Skip to content

Commit a5e780b

Browse files
authored
solana transfer ownership (#1316)
* mcms registry * tests * tests * tests * tests * tests * get authority * add transfer * tests * tests * tests * tests * tests
1 parent 8812d36 commit a5e780b

24 files changed

Lines changed: 1090 additions & 115 deletions

File tree

chains/evm/deployment/fastcurse_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func TestFastCurse(t *testing.T) {
155155
evmChain1 := env.BlockChains.EVMChains()[chain1]
156156
evmChain2 := env.BlockChains.EVMChains()[chain2]
157157
output, err := cs.Apply(*env, deploy.MCMSDeploymentConfig{
158-
Version: semver.MustParse("1.0.0"),
158+
AdapterVersion: semver.MustParse("1.0.0"),
159159
Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{
160160
chain1: {
161161
Canceller: testhelpers.SingleGroupMCMS(),

chains/evm/deployment/v1_0_0/adapters/deployer_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func TestDeployMCMS(t *testing.T) {
3838
dReg.RegisterDeployer(chainsel.FamilyEVM, deployops.MCMSVersion, evmDeployer)
3939
cs := deployops.DeployMCMS(dReg)
4040
output, err := cs.Apply(*env, deployops.MCMSDeploymentConfig{
41-
Version: semver.MustParse("1.0.0"),
41+
AdapterVersion: semver.MustParse("1.0.0"),
4242
Chains: map[uint64]deployops.MCMSDeploymentConfigPerChain{
4343
selector1: {
4444
Canceller: testhelpers.SingleGroupMCMS(),

chains/evm/deployment/v1_0_0/adapters/transfer_ownership_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestTransferOwnership(t *testing.T) {
5151
dReg.RegisterDeployer(chainsel.FamilyEVM, deploy.MCMSVersion, evmDeployer)
5252
deployMCMS := deploy.DeployMCMS(dReg)
5353
output, err := deployMCMS.Apply(*env, deploy.MCMSDeploymentConfig{
54-
Version: semver.MustParse("1.0.0"),
54+
AdapterVersion: semver.MustParse("1.0.0"),
5555
Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{
5656
selector1: {
5757
Canceller: testhelpers.SingleGroupMCMS(),
@@ -77,7 +77,7 @@ func TestTransferOwnership(t *testing.T) {
7777

7878
// deploy another timelock so that later we can transfer ownership to it from first timelock
7979
output, err = deployMCMS.Apply(*env, deploy.MCMSDeploymentConfig{
80-
Version: semver.MustParse("1.0.0"),
80+
AdapterVersion: semver.MustParse("1.0.0"),
8181
Chains: map[uint64]deploy.MCMSDeploymentConfigPerChain{
8282
selector1: {
8383
Canceller: testhelpers.SingleGroupMCMS(),
@@ -195,7 +195,7 @@ func TestTransferOwnership(t *testing.T) {
195195
cr := deploy.GetTransferOwnershipRegistry()
196196
evmAdapter := &adapters.EVMTransferOwnershipAdapter{}
197197
cr.RegisterAdapter(chainsel.FamilyEVM, transferOwnershipInput.AdapterVersion, evmAdapter)
198-
mcmsRegistry := changesets.NewMCMSReaderRegistry()
198+
mcmsRegistry := changesets.GetRegistry()
199199
evmMCMSReader := &adapters.EVMMCMSReader{}
200200
mcmsRegistry.RegisterMCMSReader(chainsel.FamilyEVM, evmMCMSReader)
201201
transferOwnershipChangeset := deploy.TransferOwnershipChangeset(cr, mcmsRegistry)

chains/evm/deployment/v1_6_0/changesets/connect_chains_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func TestConnectChains_EVM2EVM_NoMCMS(t *testing.T) {
139139
require.NoError(t, err, "Failed to create test environment")
140140
require.NotNil(t, e, "Environment should be created")
141141

142-
mcmsRegistry := cs_core.NewMCMSReaderRegistry()
142+
mcmsRegistry := cs_core.GetRegistry()
143143
dReg := deployops.GetRegistry()
144144
version := semver.MustParse("1.6.0")
145145
for _, chainSel := range chains {
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package utils
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"math/big"
8+
"time"
9+
10+
"github.com/gagliardetto/solana-go"
11+
"github.com/gagliardetto/solana-go/rpc"
12+
"github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/state"
13+
common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils"
14+
"github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore"
15+
cldf_datastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
16+
cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
17+
mcms_solana "github.com/smartcontractkit/mcms/sdk/solana"
18+
"github.com/smartcontractkit/mcms/types"
19+
)
20+
21+
const (
22+
TimelockProgramType cldf_deployment.ContractType = "RBACTimelockProgram"
23+
McmProgramType cldf_deployment.ContractType = "ManyChainMultiSigProgram"
24+
// special type for Solana that encodes PDA seed usage
25+
TimelockCompositeAddress cldf_deployment.ContractType = "RBACTimelockProgramCompositeAddress"
26+
)
27+
28+
// Common parameters for transferring ownership of a program
29+
type TransferOwnershipParams struct {
30+
Program solana.PublicKey
31+
CurrentOwner solana.PublicKey
32+
NewOwner solana.PublicKey
33+
}
34+
35+
func BuildMCMSBatchOperation(
36+
chainSelector uint64,
37+
ixns []solana.Instruction,
38+
programID string,
39+
contractType string) (types.BatchOperation, error) {
40+
txns := make([]types.Transaction, 0, len(ixns))
41+
for _, ixn := range ixns {
42+
data, err := ixn.Data()
43+
if err != nil {
44+
return types.BatchOperation{}, fmt.Errorf("failed to extract data: %w", err)
45+
}
46+
for _, account := range ixn.Accounts() {
47+
if account.IsSigner {
48+
account.IsSigner = false
49+
}
50+
}
51+
tx, err := mcms_solana.NewTransaction(
52+
programID,
53+
data,
54+
big.NewInt(0), // e.g. value
55+
ixn.Accounts(), // pass along needed accounts
56+
contractType, // some string identifying the target
57+
[]string{}, // any relevant metadata
58+
)
59+
if err != nil {
60+
return types.BatchOperation{}, fmt.Errorf("failed to create transaction: %w", err)
61+
}
62+
txns = append(txns, tx)
63+
}
64+
return types.BatchOperation{
65+
ChainSelector: types.ChainSelector(chainSelector),
66+
Transactions: txns,
67+
}, nil
68+
}
69+
70+
func GetTimelockSignerPDA(
71+
existingAddresses []cldf_datastore.AddressRef,
72+
qualifier string) solana.PublicKey {
73+
timelockProgram := datastore.GetAddressRef(
74+
existingAddresses,
75+
TimelockProgramType,
76+
common_utils.Version_1_6_0,
77+
"",
78+
)
79+
// timelock seeds stored as a separate program type
80+
// qualifier will identify the correct timelock instance
81+
timelockSeed := datastore.GetAddressRef(
82+
existingAddresses,
83+
common_utils.RBACTimelock,
84+
common_utils.Version_1_6_0,
85+
qualifier,
86+
)
87+
return state.GetTimelockSignerPDA(
88+
solana.MustPublicKeyFromBase58(timelockProgram.Address),
89+
state.PDASeed([]byte(timelockSeed.Address)),
90+
)
91+
}
92+
93+
func GetMCMSignerPDA(
94+
existingAddresses []cldf_datastore.AddressRef,
95+
qualifier string) solana.PublicKey {
96+
mcmProgram := datastore.GetAddressRef(
97+
existingAddresses,
98+
McmProgramType,
99+
common_utils.Version_1_6_0,
100+
"",
101+
)
102+
// mcm seeds stored as a separate program type
103+
// qualifier will identify the correct mcm instance
104+
mcmSeed := datastore.GetAddressRef(
105+
existingAddresses,
106+
common_utils.ProposerManyChainMultisig,
107+
common_utils.Version_1_6_0,
108+
qualifier,
109+
)
110+
return state.GetMCMSignerPDA(
111+
solana.MustPublicKeyFromBase58(mcmProgram.Address),
112+
state.PDASeed([]byte(mcmSeed.Address)),
113+
)
114+
}
115+
116+
func GetTimelockCompositeAddress(
117+
existingAddresses []cldf_datastore.AddressRef,
118+
qualifier string) string {
119+
timelockProgram := datastore.GetAddressRef(
120+
existingAddresses,
121+
TimelockProgramType,
122+
common_utils.Version_1_6_0,
123+
"",
124+
)
125+
// timelock seeds stored as a separate program type
126+
// qualifier will identify the correct timelock instance
127+
timelockSeed := datastore.GetAddressRef(
128+
existingAddresses,
129+
common_utils.RBACTimelock,
130+
common_utils.Version_1_6_0,
131+
qualifier,
132+
)
133+
return mcms_solana.ContractAddress(
134+
solana.MustPublicKeyFromBase58(timelockProgram.Address),
135+
mcms_solana.PDASeed([]byte(timelockSeed.Address)),
136+
)
137+
}
138+
139+
func FundSolanaAccounts(
140+
ctx context.Context,
141+
accounts []solana.PublicKey,
142+
solAmount uint64,
143+
solanaGoClient *rpc.Client,
144+
) error {
145+
var sigs = make([]solana.Signature, 0, len(accounts))
146+
for _, account := range accounts {
147+
sig, err := solanaGoClient.RequestAirdrop(
148+
ctx,
149+
account,
150+
solAmount*solana.LAMPORTS_PER_SOL,
151+
rpc.CommitmentFinalized)
152+
if err != nil {
153+
return err
154+
}
155+
sigs = append(sigs, sig)
156+
}
157+
158+
const timeout = 100 * time.Second
159+
const pollInterval = 500 * time.Millisecond
160+
161+
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
162+
defer cancel()
163+
164+
ticker := time.NewTicker(pollInterval)
165+
defer ticker.Stop()
166+
167+
remaining := len(sigs)
168+
for remaining > 0 {
169+
select {
170+
case <-timeoutCtx.Done():
171+
return errors.New("unable to find transaction within timeout")
172+
case <-ticker.C:
173+
statusRes, sigErr := solanaGoClient.GetSignatureStatuses(ctx, true, sigs...)
174+
if sigErr != nil {
175+
return sigErr
176+
}
177+
if statusRes == nil {
178+
return errors.New("Status response is nil")
179+
}
180+
if statusRes.Value == nil {
181+
return errors.New("Status response value is nil")
182+
}
183+
184+
unfinalizedCount := 0
185+
for _, res := range statusRes.Value {
186+
if res == nil || res.ConfirmationStatus == rpc.ConfirmationStatusFinalized {
187+
unfinalizedCount++
188+
}
189+
}
190+
remaining = unfinalizedCount
191+
}
192+
}
193+
return nil
194+
}

chains/solana/deployment/v1_6_0/operations/fee_quoter/fee_quoter.go

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
1616
cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
1717
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
18+
"github.com/smartcontractkit/mcms/types"
1819
)
1920

2021
var ContractType cldf_deployment.ContractType = "FeeQuoter"
@@ -51,17 +52,19 @@ var Initialize = operations.NewOperation(
5152
Version,
5253
"Initializes the FeeQuoter 1.6.0 contract",
5354
func(b operations.Bundle, chain cldf_solana.Chain, input Params) (sequences.OnChainOutput, error) {
55+
fee_quoter.SetProgramID(input.FeeQuoter)
5456
programData, err := utils.GetSolProgramData(chain, input.FeeQuoter)
5557
if err != nil {
5658
return sequences.OnChainOutput{}, fmt.Errorf("failed to get program data: %w", err)
5759
}
60+
authority := GetAuthority(chain, input.FeeQuoter)
5861
feeQuoterConfigPDA, _, _ := state.FindFqConfigPDA(input.FeeQuoter)
5962
instruction, err := fee_quoter.NewInitializeInstruction(
6063
input.MaxFeeJuelsPerMsg,
6164
input.Router,
6265
feeQuoterConfigPDA,
6366
input.LinkToken,
64-
chain.DeployerKey.PublicKey(),
67+
authority,
6568
solana.SystemProgramID,
6669
input.FeeQuoter,
6770
programData.Address,
@@ -82,6 +85,7 @@ var AddPriceUpdater = operations.NewOperation(
8285
Version,
8386
"Adds a price updater to the FeeQuoter 1.6.0 contract",
8487
func(b operations.Bundle, chain cldf_solana.Chain, input Params) (sequences.OnChainOutput, error) {
88+
fee_quoter.SetProgramID(input.FeeQuoter)
8589
authority := GetAuthority(chain, input.FeeQuoter)
8690
feeQuoterConfigPDA, _, _ := state.FindFqConfigPDA(input.FeeQuoter)
8791
offRampBillingSignerPDA, _, _ := state.FindOfframpBillingSignerPDA(input.OffRamp)
@@ -109,6 +113,7 @@ var ConnectChains = operations.NewOperation(
109113
Version,
110114
"Connects the FeeQuoter 1.6.0 contract to other chains",
111115
func(b operations.Bundle, chain cldf_solana.Chain, input ConnectChainsParams) (sequences.OnChainOutput, error) {
116+
fee_quoter.SetProgramID(input.FeeQuoter)
112117
isUpdate := false
113118
authority := GetAuthority(chain, input.FeeQuoter)
114119
feeQuoterConfigPDA, _, _ := state.FindFqConfigPDA(input.FeeQuoter)
@@ -126,7 +131,7 @@ var ConnectChains = operations.NewOperation(
126131
input.DestChainConfig,
127132
feeQuoterConfigPDA,
128133
fqRemoteChainPDA,
129-
chain.DeployerKey.PublicKey(),
134+
authority,
130135
).ValidateAndBuild()
131136
if err != nil {
132137
return sequences.OnChainOutput{}, fmt.Errorf("failed to build update dest chain instruction: %w", err)
@@ -156,8 +161,89 @@ var ConnectChains = operations.NewOperation(
156161
},
157162
)
158163

164+
var TransferOwnership = operations.NewOperation(
165+
"fee-quoter:transfer-ownership",
166+
Version,
167+
"Transfers ownership of the FeeQuoter 1.6.0 contract to a new authority",
168+
func(b operations.Bundle, chain cldf_solana.Chain, input utils.TransferOwnershipParams) (sequences.OnChainOutput, error) {
169+
fee_quoter.SetProgramID(input.Program)
170+
authority := GetAuthority(chain, input.Program)
171+
if authority != input.CurrentOwner {
172+
return sequences.OnChainOutput{}, fmt.Errorf("current owner %s does not match on-chain authority %s", input.CurrentOwner.String(), authority.String())
173+
}
174+
configPDA, _, _ := state.FindConfigPDA(input.Program)
175+
ixn, err := fee_quoter.NewTransferOwnershipInstruction(
176+
input.NewOwner,
177+
configPDA,
178+
authority,
179+
).ValidateAndBuild()
180+
if err != nil {
181+
return sequences.OnChainOutput{}, fmt.Errorf("failed to build add dest chain instruction: %w", err)
182+
}
183+
if authority != chain.DeployerKey.PublicKey() {
184+
batches, err := utils.BuildMCMSBatchOperation(
185+
chain.Selector,
186+
[]solana.Instruction{ixn},
187+
input.Program.String(),
188+
ContractType.String(),
189+
)
190+
if err != nil {
191+
return sequences.OnChainOutput{}, fmt.Errorf("failed to execute or create batch: %w", err)
192+
}
193+
return sequences.OnChainOutput{BatchOps: []types.BatchOperation{batches}}, nil
194+
}
195+
196+
err = chain.Confirm([]solana.Instruction{ixn})
197+
if err != nil {
198+
return sequences.OnChainOutput{}, fmt.Errorf("failed to confirm add price updater: %w", err)
199+
}
200+
return sequences.OnChainOutput{}, nil
201+
},
202+
)
203+
204+
var AcceptOwnership = operations.NewOperation(
205+
"fee-quoter:accept-ownership",
206+
Version,
207+
"Accepts ownership of the FeeQuoter 1.6.0 contract",
208+
func(b operations.Bundle, chain cldf_solana.Chain, input utils.TransferOwnershipParams) (sequences.OnChainOutput, error) {
209+
fee_quoter.SetProgramID(input.Program)
210+
configPDA, _, _ := state.FindConfigPDA(input.Program)
211+
ixn, err := fee_quoter.NewAcceptOwnershipInstruction(
212+
configPDA,
213+
input.NewOwner,
214+
).ValidateAndBuild()
215+
if err != nil {
216+
return sequences.OnChainOutput{}, fmt.Errorf("failed to build add dest chain instruction: %w", err)
217+
}
218+
if input.NewOwner != chain.DeployerKey.PublicKey() {
219+
batches, err := utils.BuildMCMSBatchOperation(
220+
chain.Selector,
221+
[]solana.Instruction{ixn},
222+
input.Program.String(),
223+
ContractType.String(),
224+
)
225+
if err != nil {
226+
return sequences.OnChainOutput{}, fmt.Errorf("failed to execute or create batch: %w", err)
227+
}
228+
return sequences.OnChainOutput{BatchOps: []types.BatchOperation{batches}}, nil
229+
}
230+
231+
err = chain.Confirm([]solana.Instruction{ixn})
232+
if err != nil {
233+
return sequences.OnChainOutput{}, fmt.Errorf("failed to confirm add price updater: %w", err)
234+
}
235+
return sequences.OnChainOutput{}, nil
236+
},
237+
)
238+
159239
func GetAuthority(chain cldf_solana.Chain, program solana.PublicKey) solana.PublicKey {
160-
return chain.DeployerKey.PublicKey()
240+
programData := fee_quoter.Config{}
241+
feeQuoterConfigPDA, _, _ := state.FindFqConfigPDA(program)
242+
err := chain.GetAccountDataBorshInto(context.Background(), feeQuoterConfigPDA, &programData)
243+
if err != nil {
244+
return chain.DeployerKey.PublicKey()
245+
}
246+
return programData.Owner
161247
}
162248

163249
type Params struct {

0 commit comments

Comments
 (0)