Repository: smartcontractkit/mcms
Pull request #329: [DPA-1519]: feat(solana): allow override authority on Configurer
Author: login: graham-chainlink
Created: 2025-03-05T14:55:02Z | Merged: 2025-03-05T17:16:32Z

When executing SetConfig via MCMS, the authority has to be the timelock signer, the current implementation does not allow any override and only accepts the `auth` or deploykey as the authority.

JIRA: https://smartcontract-it.atlassian.net/browse/DPA-1519


Diff:
diff --git a/.changeset/wise-gorillas-care.md b/.changeset/wise-gorillas-care.md
new file mode 100644
index 00000000..c97bb723
--- /dev/null
+++ b/.changeset/wise-gorillas-care.md
@@ -0,0 +1,5 @@
+---
+"@smartcontractkit/mcms": minor
+---
+
+feat(solana): override authority on Configurer
diff --git a/sdk/solana/configurer.go b/sdk/solana/configurer.go
index 0c74d184..7063d2a7 100644
--- a/sdk/solana/configurer.go
+++ b/sdk/solana/configurer.go
@@ -21,10 +21,11 @@ var _ sdk.Configurer = &Configurer{}
 // Configurer configures the MCM contract for Solana chains.
 type Configurer struct {
 	instructionCollection
-	chainSelector types.ChainSelector
-	client        *rpc.Client
-	auth          solana.PrivateKey
-	skipSend      bool
+	chainSelector    types.ChainSelector
+	client           *rpc.Client
+	auth             solana.PrivateKey
+	skipSend         bool
+	authorityAccount solana.PublicKey
 }
 
 // NewConfigurer creates a new Configurer for Solana chains.
@@ -32,15 +33,20 @@ type Configurer struct {
 // options:
 //
 //	WithDoNotSendInstructionsOnChain: when selected, the Configurer instance will not
-//		send the Solana instructions to the blockchain.
+//			send the Solana instructions to the blockchain.
+//
+//	WithAuthorityAccount: sets the authorityAccount for the MCM contract. Defaults to the `auth` field if unset.
+//		Main use case is to allow authorityAccount to be overridden when processing the SetConfig with MCMS where
+//		the authorityAccount has to be the timelock signer.
 func NewConfigurer(
 	client *rpc.Client, auth solana.PrivateKey, chainSelector types.ChainSelector, options ...configurerOption,
 ) *Configurer {
 	configurer := &Configurer{
-		client:        client,
-		auth:          auth,
-		chainSelector: chainSelector,
-		skipSend:      false,
+		client:           client,
+		auth:             auth,
+		chainSelector:    chainSelector,
+		skipSend:         false,
+		authorityAccount: auth.PublicKey(),
 	}
 	for _, opt := range options {
 		opt(configurer)
@@ -57,6 +63,12 @@ func WithDoNotSendInstructionsOnChain() configurerOption {
 	}
 }
 
+func WithAuthorityAccount(authorityAccount solana.PublicKey) configurerOption {
+	return func(c *Configurer) {
+		c.authorityAccount = authorityAccount
+	}
+}
+
 // SetConfig sets the configuration for the MCM contract on the Solana chain.
 //
 // The list of instructions needed to set the configuration is returned in the
@@ -120,7 +132,7 @@ func (c *Configurer) SetConfig(
 		configSignersPDA,
 		rootMetadataPDA,
 		expiringRootAndOpCountPDA,
-		c.auth.PublicKey(),
+		c.authorityAccount,
 		solana.SystemProgramID))
 	if err != nil {
 		return types.TransactionResult{}, err
@@ -148,21 +160,21 @@ func (c *Configurer) preloadSigners(
 	configSignersPDA solana.PublicKey,
 ) error {
 	err := c.addInstruction("initSigners", bindings.NewInitSignersInstruction(mcmName, uint8(len(signerAddresses)), //nolint:gosec
-		configPDA, configSignersPDA, c.auth.PublicKey(), solana.SystemProgramID))
+		configPDA, configSignersPDA, c.authorityAccount, solana.SystemProgramID))
 	if err != nil {
 		return err
 	}
 
 	for i, chunkIndex := range chunkIndexes(len(signerAddresses), config.MaxAppendSignerBatchSize) {
 		err = c.addInstruction(fmt.Sprintf("appendSigners%d", i), bindings.NewAppendSignersInstruction(mcmName,
-			signerAddresses[chunkIndex[0]:chunkIndex[1]], configPDA, configSignersPDA, c.auth.PublicKey()))
+			signerAddresses[chunkIndex[0]:chunkIndex[1]], configPDA, configSignersPDA, c.authorityAccount))
 		if err != nil {
 			return err
 		}
 	}
 
 	err = c.addInstruction("finalizeSigners", bindings.NewFinalizeSignersInstruction(mcmName, configPDA,
-		configSignersPDA, c.auth.PublicKey()))
+		configSignersPDA, c.authorityAccount))
 	if err != nil {
 		return err
 	}
diff --git a/sdk/solana/configurer_test.go b/sdk/solana/configurer_test.go
index 2d57b8af..7dd2f3b9 100644
--- a/sdk/solana/configurer_test.go
+++ b/sdk/solana/configurer_test.go
@@ -26,24 +26,36 @@ func Test_NewConfigurer(t *testing.T) {
 	auth := solana.MustPrivateKeyFromBase58("DmPfeHBC8Brf8s5qQXi25bmJ996v6BHRtaLc6AH51yFGSqQpUMy1oHkbbXobPNBdgGH2F29PAmoq9ZZua4K9vCc")
 	chainSelector := types.ChainSelector(cselectors.SOLANA_DEVNET.Selector)
 
+	newAuthorityAccount := solana.NewWallet().PublicKey()
+
 	tests := []struct {
-		name                string
-		constructorFn       func() *Configurer
-		wantSkipTransaction bool
+		name                 string
+		constructorFn        func() *Configurer
+		wantSkipTransaction  bool
+		wantAuthorityAccount solana.PublicKey
 	}{
 		{
 			name: "standard args",
 			constructorFn: func() *Configurer {
 				return NewConfigurer(client, auth, chainSelector)
 			},
-			wantSkipTransaction: false,
+			wantSkipTransaction:  false,
+			wantAuthorityAccount: auth.PublicKey(),
 		},
 		{
 			name: "skip transaction option",
 			constructorFn: func() *Configurer {
 				return NewConfigurer(client, auth, chainSelector, WithDoNotSendInstructionsOnChain())
 			},
-			wantSkipTransaction: true,
+			wantSkipTransaction:  true,
+			wantAuthorityAccount: auth.PublicKey(),
+		},
+		{
+			name: "override authorityAccount",
+			constructorFn: func() *Configurer {
+				return NewConfigurer(client, auth, chainSelector, WithAuthorityAccount(newAuthorityAccount))
+			},
+			wantAuthorityAccount: newAuthorityAccount,
 		},
 	}
 
@@ -54,6 +66,7 @@ func Test_NewConfigurer(t *testing.T) {
 
 			require.NotNil(t, configurer)
 			require.Equal(t, tt.wantSkipTransaction, configurer.skipSend)
+			require.Equal(t, tt.wantAuthorityAccount, configurer.authorityAccount)
 		})
 	}
 }
@@ -67,6 +80,7 @@ func TestConfigurer_SetConfig(t *testing.T) {
 	require.NoError(t, err)
 	defaultMcmConfig := &types.Config{Quorum: 1, Signers: []common.Address{common.HexToAddress("0x1")}}
 	clearRoot := false
+	newAuthorityAccount := solana.NewWallet().PublicKey()
 
 	tests := []struct {
 		name             string
@@ -118,6 +132,20 @@ func TestConfigurer_SetConfig(t *testing.T) {
 				bindings.NewSetConfigInstructionBuilder().Build(),
 			},
 		},
+		{
+			name:      "success - override authorityAccount acciybt",
+			auth:      auth,
+			options:   []configurerOption{WithDoNotSendInstructionsOnChain(), WithAuthorityAccount(newAuthorityAccount)},
+			mcmConfig: defaultMcmConfig,
+			setup:     func(t *testing.T, configurer *Configurer, mockJSONRPCClient *mocks.JSONRPCClient) { t.Helper() },
+			wantHash:  "",
+			wantInstructions: []solana.Instruction{
+				bindings.NewInitSignersInstructionBuilder().Build(),
+				bindings.NewAppendSignersInstructionBuilder().Build(),
+				bindings.NewFinalizeSignersInstructionBuilder().Build(),
+				bindings.NewSetConfigInstructionBuilder().Build(),
+			},
+		},
 		{
 			name:      "failure: too many signers",
 			auth:      auth,


Reviews:
user: login: graham-chainlink
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T14:57:03Z
user: login: ecPablo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T15:30:15Z
user: login: graham-chainlink
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T15:40:26Z
user: login: ecPablo
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-05T15:43:39Z
user: login: gustavogama-cll
body: what's the use case for this? IIRC, in the review of the original PR, i remember we decided we wouldn't need something like this.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T15:46:46Z

Review comments:
diff_hunk: @@ -118,6 +132,20 @@ func TestConfigurer_SetConfig(t *testing.T) {
 				bindings.NewSetConfigInstructionBuilder().Build(),
 			},
 		},
+		{
+			name:      "success - override authority",
+			auth:      auth,
path: sdk/solana/configurer_test.go
user: login: graham-chainlink
body: not super happy with the current testing coverage as it [ignores](https://github.com/smartcontractkit/mcms/blob/b04fa9e31f7b4f4de282b986c1b96e7b0aaa5eaf/sdk/solana/configurer_test.go#L243-L243) a bunch of relevant details, but something to come back and improve.
created_at: 2025-03-05T14:57:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,14 +25,19 @@ type Configurer struct {
 	client        *rpc.Client
 	auth          solana.PrivateKey
 	skipSend      bool
+	authority     solana.PublicKey
path: sdk/solana/configurer.go
user: login: ecPablo
body: Can we rename this to something a bit different from `auth` ? It can be a bit confusing to have both `auth` and `authority` as possible params?
created_at: 2025-03-05T15:30:05Z
author_association: COLLABORATOR
diff_hunk: @@ -25,14 +25,19 @@ type Configurer struct {
 	client        *rpc.Client
 	auth          solana.PrivateKey
 	skipSend      bool
+	authority     solana.PublicKey
path: sdk/solana/configurer.go
user: login: graham-chainlink
body: I renamed it to AuthorityAccount per the reference i can see in the solana sdk 
created_at: 2025-03-05T15:40:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-05T14:55:05Z
body: ###  🦋  Changeset detected

Latest commit: 635b70a0d0fd0d93002a914590591ca418b5af62

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                   | Type  |
| ---------------------- | ----- |
| @smartcontractkit/mcms | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/smartcontractkit/mcms/new/ggoh/configure-authority?filename=.changeset/lucky-bats-sneeze.md&value=---%0A%22%40smartcontractkit%2Fmcms%22%3A%20patch%0A---%0A%0A%5BDPA-1519%5D%3A%20feat(solana)%3A%20allow%20override%20authority%20on%20Configurer%0A)


author_association: NONE
user: login: ecPablo
created_at: 2025-03-05T15:48:15Z
body: > what's the use case for this? IIRC, in the review of the original PR, i remember we decided we wouldn't need something like this.

So the main reason for this, is because in order to call setconfig via an MCMS proposal, we need to override the authority to be the timelock signer PDA instead of the deployer keys.
author_association: COLLABORATOR
user: login: graham-chainlink
created_at: 2025-03-05T15:49:06Z
body: > what's the use case for this? IIRC, in the review of the original PR, i remember we decided we wouldn't need something like this.

@gustavogama-cll  the use case is related to my [SetConfig changset PR](https://github.com/smartcontractkit/chainlink/pull/16554/commits/f33565d0fe4df26a8954226dca4a78b62da7d54d), currrently the test is failing because we have to provide the timelock signer PDA as the authority instead of the deployerKey 
author_association: CONTRIBUTOR
user: login: gustavogama-cll
created_at: 2025-03-05T15:59:47Z
body: > > what's the use case for this? IIRC, in the review of the original PR, i remember we decided we wouldn't need something like this.
> 
> @gustavogama-cll the use case is related to my [SetConfig changset PR](https://github.com/smartcontractkit/chainlink/pull/16554/commits/f33565d0fe4df26a8954226dca4a78b62da7d54d), currrently the test is failing because we have to provide the timelock signer PDA as the authority instead of the deployerKey

Hmm... then I think we made a bad decision to change the functional option after [this discussion](https://github.com/smartcontractkit/mcms/pull/310#discussion_r1964851859). There doesn't seem to be a valid use case for using the two options (`WithDoNotSendTransaction` and `WithAuthority`) independently.
author_association: CONTRIBUTOR
user: login: cl-sonarqube-production[bot]
created_at: 2025-03-05T16:03:11Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarqube.main.prod.cldev.sh/dashboard?id=smartcontractkit_mcms&pullRequest=329) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_mcms&pullRequest=329&resolved=false) [0 New issues](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_mcms&pullRequest=329&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_mcms&fixedInPullRequest=329) [0 Fixed issues](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_mcms&fixedInPullRequest=329)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_mcms&pullRequest=329&issueStatuses=ACCEPTED) [0 Accepted issues](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_mcms&pullRequest=329&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.main.prod.cldev.sh/security_hotspots?id=smartcontractkit_mcms&pullRequest=329) [0 Security Hotspots](https://sonarqube.main.prod.cldev.sh/security_hotspots?id=smartcontractkit_mcms&pullRequest=329)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_mcms&pullRequest=329&metric=new_coverage&view=list) [100.0% Coverage on New Code](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_mcms&pullRequest=329&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_mcms&pullRequest=329&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_mcms&pullRequest=329&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://sonarqube.main.prod.cldev.sh/dashboard?id=smartcontractkit_mcms&pullRequest=329)


author_association: NONE
user: login: graham-chainlink
created_at: 2025-03-05T16:34:45Z
body: > Hmm... then I think we made a bad decision to change the functional option after [this discussion](https://github.com/smartcontractkit/mcms/pull/310#discussion_r1964851859). There doesn't seem to be a valid use case for using the two options (`WithDoNotSendTransaction` and `WithAuthority`) independently.

I wonder should we combine them into 1 then, like `withMCMSExecution(accountAuthority)`? @ecPablo @gustavogama-cll  
author_association: CONTRIBUTOR
user: login: gustavogama-cll
created_at: 2025-03-05T16:41:52Z
body: > I wonder should we combine them into 1 then, like withMCMSExecution(accountAuthority)? @ecPablo @gustavogama-cll 

I'd leave it as is. `WithMCMSExecution` sounds odd. At least the current options name meaningful names. And, who knows, maybe we'll find use cases for each in the future.
author_association: CONTRIBUTOR