Repository: smartcontractkit/chainlink
Pull request #16867: feat: check for access controller initialization to make mcms deploym…
Author: login: ecPablo
Created: 2025-03-20T14:25:34Z | Merged: 2025-03-20T18:13:04Z

This PR makes the mcms changeset consistent with all CCIP changesets where they are idempotent in the sense that if an account has already been initialized, the initialization is skipped. This was not the case with the access controllers as they were being created each time the function was called without checking if an address already existed in the address book for this contract type and version


Diff:
diff --git a/deployment/common/changeset/deploy_mcms_with_timelock_test.go b/deployment/common/changeset/deploy_mcms_with_timelock_test.go
index 785cfafedb5..dd615d7eb00 100644
--- a/deployment/common/changeset/deploy_mcms_with_timelock_test.go
+++ b/deployment/common/changeset/deploy_mcms_with_timelock_test.go
@@ -268,6 +268,95 @@ func TestDeployMCMSWithTimelockV2(t *testing.T) {
 		timelockSignerPDA(solanaState0.TimelockProgram, solanaState0.TimelockSeed))
 }
 
+// TestDeployMCMSWithTimelockV2SkipInit tests calling the deploy changeset when accounts have already been initialized
+func TestDeployMCMSWithTimelockV2SkipInitSolana(t *testing.T) {
+	t.Parallel()
+	// --- arrange ---
+	log := logger.TestLogger(t)
+	envConfig := memory.MemoryEnvironmentConfig{Chains: 0, SolChains: 1}
+	env := memory.NewMemoryEnvironment(t, log, zapcore.InfoLevel, envConfig)
+	solanaSelectors := env.AllChainSelectorsSolana()
+	changesetConfig := map[uint64]commontypes.MCMSWithTimelockConfigV2{
+		solanaSelectors[0]: {
+			Proposer: mcmstypes.Config{
+				Quorum: 1,
+				Signers: []common.Address{
+					common.HexToAddress("0x0000000000000000000000000000000000000021"),
+					common.HexToAddress("0x0000000000000000000000000000000000000022"),
+				},
+				GroupSigners: []mcmstypes.Config{
+					{
+						Quorum: 2,
+						Signers: []common.Address{
+							common.HexToAddress("0x0000000000000000000000000000000000000023"),
+							common.HexToAddress("0x0000000000000000000000000000000000000024"),
+							common.HexToAddress("0x0000000000000000000000000000000000000025"),
+						},
+						GroupSigners: []mcmstypes.Config{
+							{
+								Quorum: 1,
+								Signers: []common.Address{
+									common.HexToAddress("0x0000000000000000000000000000000000000026"),
+								},
+								GroupSigners: []mcmstypes.Config{},
+							},
+						},
+					},
+				},
+			},
+			Canceller: mcmstypes.Config{
+				Quorum: 1,
+				Signers: []common.Address{
+					common.HexToAddress("0x0000000000000000000000000000000000000027"),
+				},
+				GroupSigners: []mcmstypes.Config{},
+			},
+			Bypasser: mcmstypes.Config{
+				Quorum: 1,
+				Signers: []common.Address{
+					common.HexToAddress("0x0000000000000000000000000000000000000028"),
+					common.HexToAddress("0x0000000000000000000000000000000000000029"),
+				},
+				GroupSigners: []mcmstypes.Config{},
+			},
+			TimelockMinDelay: big.NewInt(2),
+		},
+	}
+	configuredChangeset := commonchangeset.Configure(
+		deployment.CreateLegacyChangeSet(commonchangeset.DeployMCMSWithTimelockV2),
+		changesetConfig,
+	)
+	commonchangeset.SetPreloadedSolanaAddresses(t, env, solanaSelectors[0])
+	// --- act ---
+	updatedEnv, err := commonchangeset.Apply(t, env, nil, configuredChangeset)
+	require.NoError(t, err)
+
+	solanaState, err := mcmschangesetstate.MaybeLoadMCMSWithTimelockStateSolana(updatedEnv, solanaSelectors)
+	require.NoError(t, err)
+
+	// Call deploy again, seeds and addresses from original state should not change
+	updatedEnvReTriggered, err := commonchangeset.Apply(t, updatedEnv, nil, configuredChangeset)
+	require.NoError(t, err)
+	solanaStateNew, err := mcmschangesetstate.MaybeLoadMCMSWithTimelockStateSolana(updatedEnvReTriggered, solanaSelectors)
+	require.NoError(t, err)
+
+	// --- assert ---
+	require.Len(t, solanaState, 1)
+	stateOld := solanaState[solanaSelectors[0]]
+	stateNew := solanaStateNew[solanaSelectors[0]]
+	require.Equal(t, stateOld.TimelockSeed, stateNew.TimelockSeed)
+	require.Equal(t, stateOld.TimelockProgram, stateNew.TimelockProgram)
+	require.Equal(t, stateOld.BypasserAccessControllerAccount, stateNew.BypasserAccessControllerAccount)
+	require.Equal(t, stateOld.CancellerAccessControllerAccount, stateNew.CancellerAccessControllerAccount)
+	require.Equal(t, stateOld.ExecutorAccessControllerAccount, stateNew.ExecutorAccessControllerAccount)
+	require.Equal(t, stateOld.ProposerAccessControllerAccount, stateNew.ProposerAccessControllerAccount)
+	require.Equal(t, stateOld.McmProgram, stateNew.McmProgram)
+	require.Equal(t, stateOld.BypasserMcmSeed, stateNew.BypasserMcmSeed)
+	require.Equal(t, stateOld.CancellerMcmSeed, stateNew.CancellerMcmSeed)
+	require.Equal(t, stateOld.ProposerMcmSeed, stateNew.ProposerMcmSeed)
+	require.Equal(t, stateOld.AccessControllerProgram, stateNew.AccessControllerProgram)
+}
+
 // ----- helpers -----
 
 func mcmSignerPDA(programID solana.PublicKey, seed mcmschangesetstate.PDASeed) string {
diff --git a/deployment/common/changeset/internal/solana/access_controller.go b/deployment/common/changeset/internal/solana/access_controller.go
index 52ddaa5d99b..e139ff75997 100644
--- a/deployment/common/changeset/internal/solana/access_controller.go
+++ b/deployment/common/changeset/internal/solana/access_controller.go
@@ -66,10 +66,29 @@ func initAccessController(
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	_, accessControllerAccountSeed, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get account controller state: %w", err)
+	}
+
+	accessControllerAccount := solana.PublicKeyFromBytes(accessControllerAccountSeed[:])
+	if !accessControllerAccount.IsZero() {
+		var data accessControllerBindings.AccessController
+		err = solanaUtils.GetAccountDataBorshInto(e.GetContext(), chain.Client, accessControllerAccount, rpc.CommitmentConfirmed, &data)
+		if err == nil {
+			e.Logger.Infow("access controller already initialized, skipping initialization", "chain", chain.String())
+			return nil
+		}
+
+		return fmt.Errorf("unable to read access controller account config %s", accessControllerAccount.String())
+	}
+
+	e.Logger.Infow("access controller not initialized, initializing", "chain", chain.String())
+
 	programID := chainState.AccessControllerProgram
 	accessControllerBindings.SetProgramID(programID)
 
-	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
 	log := logger.With(e.Logger, "chain", chain.String(), "contract", typeAndVersion.String(), "programID", programID)
 
 	account, err := solana.NewRandomPrivateKey() // FIXME: what should we do with the account private key?
diff --git a/deployment/common/changeset/internal/solana/mcm.go b/deployment/common/changeset/internal/solana/mcm.go
index a3193a71446..99ba5ef84c5 100644
--- a/deployment/common/changeset/internal/solana/mcm.go
+++ b/deployment/common/changeset/internal/solana/mcm.go
@@ -13,6 +13,9 @@ import (
 
 	mcmBindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/mcm"
 	"github.com/smartcontractkit/chainlink-common/pkg/logger"
+
+	solanaUtils "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common"
+
 	"github.com/smartcontractkit/chainlink/deployment"
 	"github.com/smartcontractkit/chainlink/deployment/common/changeset/state"
 	commontypes "github.com/smartcontractkit/chainlink/deployment/common/types"
@@ -70,12 +73,29 @@ func initMCM(
 	mcmBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	mcmProgram, mcmSeed, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get mcm state: %w", err)
+	}
+
+	if mcmSeed != (state.PDASeed{}) {
+		mcmConfigPDA := state.GetMCMConfigPDA(mcmProgram, mcmSeed)
+		var data mcmBindings.MultisigConfig
+		err = solanaUtils.GetAccountDataBorshInto(env.GetContext(), chain.Client, mcmConfigPDA, rpc.CommitmentConfirmed, &data)
+		if err == nil {
+			env.Logger.Infow("mcm config already initialized, skipping initialization", "chain", chain.String())
+			return nil
+		}
+		return fmt.Errorf("unable to read mcm ConfigPDA account config %s", mcmConfigPDA.String())
+	}
+
+	env.Logger.Infow("mcm config not initialized, initializing", "chain", chain.String())
 	log := logger.With(env.Logger, "chain", chain.String(), "contract", typeAndVersion.String())
 
 	seed := randomSeed()
 	log.Infow("generated MCM seed", "seed", string(seed[:]))
 
-	err := initializeMCM(env, chain, programID, seed)
+	err = initializeMCM(env, chain, programID, seed)
 	if err != nil {
 		return fmt.Errorf("failed to initialize mcm: %w", err)
 	}
diff --git a/deployment/common/changeset/internal/solana/timelock.go b/deployment/common/changeset/internal/solana/timelock.go
index 58d35923470..38b18c75170 100644
--- a/deployment/common/changeset/internal/solana/timelock.go
+++ b/deployment/common/changeset/internal/solana/timelock.go
@@ -11,6 +11,7 @@ import (
 
 	timelockBindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/timelock"
 	"github.com/smartcontractkit/chainlink-common/pkg/logger"
+
 	"github.com/smartcontractkit/chainlink/deployment"
 	"github.com/smartcontractkit/chainlink/deployment/common/changeset/state"
 	commontypes "github.com/smartcontractkit/chainlink/deployment/common/types"
@@ -68,12 +69,29 @@ func initTimelock(
 	timelockBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(commontypes.RBACTimelock, deployment.Version1_0_0)
+	timelockProgram, timelockSeed, err := chainState.GetStateFromType(commontypes.RBACTimelock)
+	if err != nil {
+		return fmt.Errorf("failed to get timelock state: %w", err)
+	}
+
+	if (timelockSeed != state.PDASeed{}) {
+		timelockConfigPDA := state.GetTimelockConfigPDA(timelockProgram, timelockSeed)
+		var timelockConfig timelockBindings.Config
+		err = chain.GetAccountDataBorshInto(e.GetContext(), timelockConfigPDA, &timelockConfig)
+		if err == nil {
+			e.Logger.Infow("timelock config already initialized, skipping initialization", "chain", chain.String())
+			return nil
+		}
+		return fmt.Errorf("unable to read timelock ConfigPDA account config %s", timelockConfigPDA.String())
+	}
+
+	e.Logger.Infow("timelock config not initialized, initializing", "chain", chain.String())
 	log := logger.With(e.Logger, "chain", chain.String(), "contract", typeAndVersion.String())
 
 	seed := randomSeed()
 	log.Infow("generated Timelock seed", "seed", string(seed[:]))
 
-	err := initializeTimelock(e, chain, programID, seed, chainState, minDelay)
+	err = initializeTimelock(e, chain, programID, seed, chainState, minDelay)
 	if err != nil {
 		return fmt.Errorf("failed to initialize timelock: %w", err)
 	}


Reviews:
user: login: tt-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:32:23Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:35:49Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:00:29Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:04:15Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:04:30Z
user: login: tt-cll
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:06:49Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T15:18:46Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:13:25Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:14:09Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:15:58Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:16:59Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:41:33Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:42:26Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:42:33Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:42:59Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:43:10Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:45:46Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:47:48Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:52:24Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:52:55Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:54:01Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:55:09Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:57:06Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:57:41Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:00:18Z
user: login: yashnevatia
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:02:22Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:04:35Z
user: login: ecPablo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:13:24Z
user: login: gustavogama-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:20:35Z
user: login: gustavogama-cll
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:31:30Z
user: login: yashnevatia
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:32:47Z
user: login: tt-cll
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:38:27Z

Review comments:
diff_hunk: @@ -66,10 +74,29 @@ func initAccessController(
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	// Check if access controller has already been initialized
+	addresses, err := addressBook.AddressesForChain(chain.Selector)
+	if err != nil {
+		return fmt.Errorf("failed to get addresses for chain %v from environment: %w", chain.Selector, err)
+	}
+	addr, err := findExistingAccessController(addresses, typeAndVersion)
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: tt-cll
body: do we need to search the address book? Can't we just use `chainState.AccessControllerProgram`?
created_at: 2025-03-20T14:32:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,10 +74,29 @@ func initAccessController(
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	// Check if access controller has already been initialized
+	addresses, err := addressBook.AddressesForChain(chain.Selector)
+	if err != nil {
+		return fmt.Errorf("failed to get addresses for chain %v from environment: %w", chain.Selector, err)
+	}
+	addr, err := findExistingAccessController(addresses, typeAndVersion)
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: ecPablo
body: hmm I think we cannot check with the access controller program but we can check for the role accounts: `ProposerAccessControllerAccount, ExecutorAccessControllerAccount, etc`. Let me refactor this 
created_at: 2025-03-20T14:35:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,6 +58,14 @@ func deployAccessControllerProgram(
 
 	return nil
 }
+func findExistingAccessController(addresses map[string]deployment.TypeAndVersion, accessControllerType deployment.TypeAndVersion) (string, error) {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: gustavogama-cll
body: missing line break before `func`
created_at: 2025-03-20T15:00:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,6 +58,14 @@ func deployAccessControllerProgram(
 
 	return nil
 }
+func findExistingAccessController(addresses map[string]deployment.TypeAndVersion, accessControllerType deployment.TypeAndVersion) (string, error) {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: ecPablo
body: added https://github.com/smartcontractkit/chainlink/pull/16867/commits/c02b9563c2b90fce5b5f74a301ff8941a662a4ee
created_at: 2025-03-20T15:04:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,10 +74,29 @@ func initAccessController(
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	// Check if access controller has already been initialized
+	addresses, err := addressBook.AddressesForChain(chain.Selector)
+	if err != nil {
+		return fmt.Errorf("failed to get addresses for chain %v from environment: %w", chain.Selector, err)
+	}
+	addr, err := findExistingAccessController(addresses, typeAndVersion)
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: ecPablo
body: added the change here https://github.com/smartcontractkit/chainlink/pull/16867/commits/f30a6a541f42f77ba92f5615410386f6864d242e
created_at: 2025-03-20T15:04:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,17 +59,42 @@ func deployAccessControllerProgram(
 	return nil
 }
 
+func findAccessControllerAccount(chainState *state.MCMSWithTimelockStateSolana, typeAndVersion deployment.TypeAndVersion) (solana.PublicKey, error) {
+	switch typeAndVersion {
+	case deployment.NewTypeAndVersion(commontypes.ProposerAccessControllerAccount, deployment.Version1_0_0):
+		return chainState.ProposerAccessControllerAccount, nil
+	case deployment.NewTypeAndVersion(commontypes.ExecutorAccessControllerAccount, deployment.Version1_0_0):
+		return chainState.ExecutorAccessControllerAccount, nil
+	case deployment.NewTypeAndVersion(commontypes.CancellerAccessControllerAccount, deployment.Version1_0_0):
+		return chainState.CancellerAccessControllerAccount, nil
+	case deployment.NewTypeAndVersion(commontypes.BypasserAccessControllerAccount, deployment.Version1_0_0):
+		return chainState.BypasserAccessControllerAccount, nil
+	default:
+		return solana.PublicKey{}, errors.New("unknown access controller account type")
+	}
+}
+
 func initAccessController(
 	e deployment.Environment, chainState *state.MCMSWithTimelockStateSolana, contractType deployment.ContractType,
 	chain deployment.SolChain, addressBook deployment.AddressBook,
 ) error {
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	pubKeyRole, err := findAccessControllerAccount(chainState, typeAndVersion)
+	if err == nil && !pubKeyRole.IsZero() {
+		var data accessControllerBindings.AccessController
+		err = solanaUtils.GetAccountDataBorshInto(e.GetContext(), chain.Client, pubKeyRole, rpc.CommitmentConfirmed, &data)
+		if err == nil {
+			e.Logger.Infow("access controller already initialized, skipping initialization", "chain", chain.String())
+			return nil
+		}
+	}
+
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: gustavogama-cll
body: ```suggestion

	_, accessControllerAccount, err := chainState.GetStateFromType(contractType)
	if err != nil {
		return fmt.Errorf("failed to get account controller state: %w", err)
	}
	if solana.PublicKey(accessControllerAccount).IsZero() {
		var data accessControllerBindings.AccessController
		err = solanaUtils.GetAccountDataBorshInto(e.GetContext(), chain.Client,
			solana.PublicKey(accessControllerAccount), rpc.CommitmentConfirmed, &data)
		if err == nil {
			e.Logger.Infow("access controller already initialized, skipping initialization", "chain", chain.String())
			return nil
		}

		e.Logger.Warnw("unable to read access controller account", "account", solana.PublicKey(accessControllerAccount))
	}

```
created_at: 2025-03-20T15:18:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,17 +59,41 @@ func deployAccessControllerProgram(
 	return nil
 }
 
+func findAccessControllerAccount(chainState *state.MCMSWithTimelockStateSolana, typeAndVersion deployment.TypeAndVersion) (solana.PublicKey, error) {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: gustavogama-cll
body: I don't think we need to use this. See my suggestion in the previous comment.
created_at: 2025-03-20T16:13:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,6 +61,18 @@ func deployMCMProgram(
 	return nil
 }
 
+func findMcmAccount(chainState *state.MCMSWithTimelockStateSolana, typeAndVersion deployment.TypeAndVersion) (solana.PublicKey, error) {
path: deployment/common/changeset/internal/solana/mcm.go
user: login: gustavogama-cll
body: I don't think we need this method. See my suggestion in the other comment, where we leverage the chainState for the same purpose.
created_at: 2025-03-20T16:14:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,12 +69,19 @@ func initTimelock(
 	timelockBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(commontypes.RBACTimelock, deployment.Version1_0_0)
+	timelockConfigPDA := state.GetTimelockConfigPDA(chainState.TimelockProgram, chainState.TimelockSeed)
path: deployment/common/changeset/internal/solana/timelock.go
user: login: gustavogama-cll
body: again: I think we should use a test like [this](https://github.com/smartcontractkit/chainlink/pull/16867/files#r2005882872), to make the logic consistent across the board.

(for context: this is the type of logic we use to skip the solana program deployments)
created_at: 2025-03-20T16:15:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,17 +59,41 @@ func deployAccessControllerProgram(
 	return nil
 }
 
+func findAccessControllerAccount(chainState *state.MCMSWithTimelockStateSolana, typeAndVersion deployment.TypeAndVersion) (solana.PublicKey, error) {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: ecPablo
body: If we use the GetStateFromType I think we'll be ignoring the version right? Not sure if it's super critical now, but if we ever use another mcm version we could be wrongly skipping initialization for a new version of the same program?
created_at: 2025-03-20T16:16:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,6 +61,18 @@ func deployMCMProgram(
 	return nil
 }
 
+func findMcmAccount(chainState *state.MCMSWithTimelockStateSolana, typeAndVersion deployment.TypeAndVersion) (solana.PublicKey, error) {
path: deployment/common/changeset/internal/solana/mcm.go
user: login: ecPablo
body: solved https://github.com/smartcontractkit/chainlink/pull/16867/commits/49d440198c96aa602fe4b1cd41020f8a6ecad1c7
created_at: 2025-03-20T16:41:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,12 +69,19 @@ func initTimelock(
 	timelockBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(commontypes.RBACTimelock, deployment.Version1_0_0)
+	timelockConfigPDA := state.GetTimelockConfigPDA(chainState.TimelockProgram, chainState.TimelockSeed)
path: deployment/common/changeset/internal/solana/timelock.go
user: login: ecPablo
body: solved here too https://github.com/smartcontractkit/chainlink/pull/16867/commits/49d440198c96aa602fe4b1cd41020f8a6ecad1c7
created_at: 2025-03-20T16:42:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,17 +59,41 @@ func deployAccessControllerProgram(
 	return nil
 }
 
+func findAccessControllerAccount(chainState *state.MCMSWithTimelockStateSolana, typeAndVersion deployment.TypeAndVersion) (solana.PublicKey, error) {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: ecPablo
body: https://github.com/smartcontractkit/chainlink/pull/16867/commits/49d440198c96aa602fe4b1cd41020f8a6ecad1c7
created_at: 2025-03-20T16:42:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,17 +59,41 @@ func deployAccessControllerProgram(
 	return nil
 }
 
+func findAccessControllerAccount(chainState *state.MCMSWithTimelockStateSolana, typeAndVersion deployment.TypeAndVersion) (solana.PublicKey, error) {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: ecPablo
body: Will probably need to discuss if it's worth refactoring `GetStateFromType` to accept a type and version 
created_at: 2025-03-20T16:42:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,17 +59,42 @@ func deployAccessControllerProgram(
 	return nil
 }
 
+func findAccessControllerAccount(chainState *state.MCMSWithTimelockStateSolana, typeAndVersion deployment.TypeAndVersion) (solana.PublicKey, error) {
+	switch typeAndVersion {
+	case deployment.NewTypeAndVersion(commontypes.ProposerAccessControllerAccount, deployment.Version1_0_0):
+		return chainState.ProposerAccessControllerAccount, nil
+	case deployment.NewTypeAndVersion(commontypes.ExecutorAccessControllerAccount, deployment.Version1_0_0):
+		return chainState.ExecutorAccessControllerAccount, nil
+	case deployment.NewTypeAndVersion(commontypes.CancellerAccessControllerAccount, deployment.Version1_0_0):
+		return chainState.CancellerAccessControllerAccount, nil
+	case deployment.NewTypeAndVersion(commontypes.BypasserAccessControllerAccount, deployment.Version1_0_0):
+		return chainState.BypasserAccessControllerAccount, nil
+	default:
+		return solana.PublicKey{}, errors.New("unknown access controller account type")
+	}
+}
+
 func initAccessController(
 	e deployment.Environment, chainState *state.MCMSWithTimelockStateSolana, contractType deployment.ContractType,
 	chain deployment.SolChain, addressBook deployment.AddressBook,
 ) error {
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	pubKeyRole, err := findAccessControllerAccount(chainState, typeAndVersion)
+	if err == nil && !pubKeyRole.IsZero() {
+		var data accessControllerBindings.AccessController
+		err = solanaUtils.GetAccountDataBorshInto(e.GetContext(), chain.Client, pubKeyRole, rpc.CommitmentConfirmed, &data)
+		if err == nil {
+			e.Logger.Infow("access controller already initialized, skipping initialization", "chain", chain.String())
+			return nil
+		}
+	}
+
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: ecPablo
body: solved 
created_at: 2025-03-20T16:43:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,10 +66,24 @@ func initAccessController(
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	_, accessControllerAccountSeed, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get account controller state: %w", err)
+	}
+	accessControllerAccount := solana.PublicKeyFromBytes(accessControllerAccountSeed[:])
+	if !accessControllerAccount.IsZero() {
+		var data accessControllerBindings.AccessController
+		err = solanaUtils.GetAccountDataBorshInto(e.GetContext(), chain.Client, accessControllerAccount, rpc.CommitmentConfirmed, &data)
+		if err == nil {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: gustavogama-cll
body: if this is not nil we should at least log a warning or even abort altogether
created_at: 2025-03-20T16:45:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,12 +72,25 @@ func initMCM(
 	mcmBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	mcmProgram, seedMcm, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get mcm state: %w", err)
+	}
+	mcmPubKey := state.GetMCMConfigPDA(mcmProgram, seedMcm)
+	if !mcmPubKey.IsZero() {
path: deployment/common/changeset/internal/solana/mcm.go
user: login: gustavogama-cll
body: Does this work? I think we should just check the `seedMcm` variable, not the config pda. `state.GetMCMConfigPDA(mcmProgram, []byte{})` should return a valid PDA, no?
created_at: 2025-03-20T16:47:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,10 +66,24 @@ func initAccessController(
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	_, accessControllerAccountSeed, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get account controller state: %w", err)
+	}
+	accessControllerAccount := solana.PublicKeyFromBytes(accessControllerAccountSeed[:])
+	if !accessControllerAccount.IsZero() {
+		var data accessControllerBindings.AccessController
+		err = solanaUtils.GetAccountDataBorshInto(e.GetContext(), chain.Client, accessControllerAccount, rpc.CommitmentConfirmed, &data)
+		if err == nil {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: ecPablo
body: I think a warning might be too much if it's not `nil`, because that means that the account is not initialized and thus normal initialization should happen. So it's not something to warn but an actual expected route. Maybe I'm missing something here. I still added some info logs though here https://github.com/smartcontractkit/chainlink/pull/16867/commits/212b2d482f8aac21f7d206e8d307f70c989d7cc1
created_at: 2025-03-20T16:52:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,12 +69,23 @@ func initTimelock(
 	timelockBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(commontypes.RBACTimelock, deployment.Version1_0_0)
+	timelockProgram, timelockSeed, err := chainState.GetStateFromType(commontypes.RBACTimelock)
+	if err != nil {
+		return fmt.Errorf("failed to get timelock state: %w", err)
+	}
+	timelockConfigPDA := state.GetTimelockConfigPDA(timelockProgram, timelockSeed)
path: deployment/common/changeset/internal/solana/timelock.go
user: login: gustavogama-cll
body: this _kinda_ works, unless someone inadvertently initializes a timelock with the seed `[]byte{}`.

I think the safe thing to check is whether the seed is empty:
`if (timelockSeed != state.PDASeed{}) {`

I also think we can skip the `GetAccountInfo` and `GetConfig` calls. All other changesets check for the presense of an entry in the address book, so we could do the same here if you want. But I don't mind if you want to keep it -- I'm just pointing it out for consistency's sake.
created_at: 2025-03-20T16:52:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,12 +72,25 @@ func initMCM(
 	mcmBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	mcmProgram, seedMcm, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get mcm state: %w", err)
+	}
+	mcmPubKey := state.GetMCMConfigPDA(mcmProgram, seedMcm)
+	if !mcmPubKey.IsZero() {
path: deployment/common/changeset/internal/solana/mcm.go
user: login: ecPablo
body: Yes in this case we'll still return a valid PDA but the account data should fail be there when calling `GetAccountDataBorshInto` meaning it has not been initialized (although it is a syntactically valid pda)
created_at: 2025-03-20T16:54:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,10 +66,24 @@ func initAccessController(
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	_, accessControllerAccountSeed, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get account controller state: %w", err)
+	}
+	accessControllerAccount := solana.PublicKeyFromBytes(accessControllerAccountSeed[:])
+	if !accessControllerAccount.IsZero() {
+		var data accessControllerBindings.AccessController
+		err = solanaUtils.GetAccountDataBorshInto(e.GetContext(), chain.Client, accessControllerAccount, rpc.CommitmentConfirmed, &data)
+		if err == nil {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: gustavogama-cll
body: I think that if the accessControllerAccount is not unset (`!IsZero()`) **and** we can't read it, something is very broken. If it was saved to the address book, it _must_ have the access controller account data.
created_at: 2025-03-20T16:55:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,12 +69,23 @@ func initTimelock(
 	timelockBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(commontypes.RBACTimelock, deployment.Version1_0_0)
+	timelockProgram, timelockSeed, err := chainState.GetStateFromType(commontypes.RBACTimelock)
+	if err != nil {
+		return fmt.Errorf("failed to get timelock state: %w", err)
+	}
+	timelockConfigPDA := state.GetTimelockConfigPDA(timelockProgram, timelockSeed)
path: deployment/common/changeset/internal/solana/timelock.go
user: login: ecPablo
body: Hmm I think it might still be valuable to check on-chain. Someone can just place a value manually in the addressbook and we'll consider the account initialized although the state on-chain was not. @yashnevatia share a similar snippet for the RMN:
```go
	// RMN REMOTE INITIALIZE
	var rmnRemoteConfigAccount solRmnRemote.Config
	rmnRemoteConfigPDA, _, _ := solState.FindRMNRemoteConfigPDA(rmnRemoteAddress)
	err = chain.GetAccountDataBorshInto(e.GetContext(), rmnRemoteConfigPDA, &rmnRemoteConfigAccount)
	if err != nil {
		if err2 := initializeRMNRemote(e, chain, rmnRemoteAddress); err2 != nil {
			return txns, err2
		}
	} else {
		e.Logger.Infow("RMN remote already initialized, skipping initialization", "chain", chain.String())
	}
```
created_at: 2025-03-20T16:57:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,12 +72,25 @@ func initMCM(
 	mcmBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	mcmProgram, seedMcm, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get mcm state: %w", err)
+	}
+	mcmPubKey := state.GetMCMConfigPDA(mcmProgram, seedMcm)
+	if !mcmPubKey.IsZero() {
path: deployment/common/changeset/internal/solana/mcm.go
user: login: gustavogama-cll
body: But it could also be a valid PDA config if someone ever (inadvertently or maliciously) initializes a PDA with the empty seed.
created_at: 2025-03-20T16:57:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,12 +69,23 @@ func initTimelock(
 	timelockBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(commontypes.RBACTimelock, deployment.Version1_0_0)
+	timelockProgram, timelockSeed, err := chainState.GetStateFromType(commontypes.RBACTimelock)
+	if err != nil {
+		return fmt.Errorf("failed to get timelock state: %w", err)
+	}
+	timelockConfigPDA := state.GetTimelockConfigPDA(timelockProgram, timelockSeed)
path: deployment/common/changeset/internal/solana/timelock.go
user: login: gustavogama-cll
body: Sure, no problem. Just wanted to highlight that this is how we handle _all_ evm checks (if someone adds an entry to the address book, you simply assume that the entry points to a contract of that type and version).
created_at: 2025-03-20T17:00:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,12 +72,25 @@ func initMCM(
 	mcmBindings.SetProgramID(programID)
 
 	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	mcmProgram, seedMcm, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get mcm state: %w", err)
+	}
+	mcmPubKey := state.GetMCMConfigPDA(mcmProgram, seedMcm)
+	if !mcmPubKey.IsZero() {
path: deployment/common/changeset/internal/solana/mcm.go
user: login: ecPablo
body: gotcha valid point, added the check for empty seeds here: https://github.com/smartcontractkit/chainlink/pull/16867/commits/7b5a2620be95a9671eb6639b39176cb9373e5540
created_at: 2025-03-20T17:04:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,10 +66,24 @@ func initAccessController(
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	_, accessControllerAccountSeed, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get account controller state: %w", err)
+	}
+	accessControllerAccount := solana.PublicKeyFromBytes(accessControllerAccountSeed[:])
+	if !accessControllerAccount.IsZero() {
+		var data accessControllerBindings.AccessController
+		err = solanaUtils.GetAccountDataBorshInto(e.GetContext(), chain.Client, accessControllerAccount, rpc.CommitmentConfirmed, &data)
+		if err == nil {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: ecPablo
body: Yeah it's indeed an odd condition, but I can see it happening if someone just changes the addressbook manually (either by mistake or debugging)? If that happens there's 3 options:

1. The Changeset should just not deploy the program again, but will trigger the initialization for the new accounts. Which was my expectation.
2. We just error out, we consider this a broken state.
3. The warning log.

I think if we find this is a broken state, then it might be better to just abort then, as it can be more confusing to try to initialize a pre existing address with a warning. What do you think? I just want to make sure we agree if this is either a broken state or something expected that we handle by just re initializing
created_at: 2025-03-20T17:13:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,10 +66,24 @@ func initAccessController(
 	if chainState.AccessControllerProgram.IsZero() {
 		return errors.New("access controller program is not deployed")
 	}
+	typeAndVersion := deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0)
+	_, accessControllerAccountSeed, err := chainState.GetStateFromType(contractType)
+	if err != nil {
+		return fmt.Errorf("failed to get account controller state: %w", err)
+	}
+	accessControllerAccount := solana.PublicKeyFromBytes(accessControllerAccountSeed[:])
+	if !accessControllerAccount.IsZero() {
+		var data accessControllerBindings.AccessController
+		err = solanaUtils.GetAccountDataBorshInto(e.GetContext(), chain.Client, accessControllerAccount, rpc.CommitmentConfirmed, &data)
+		if err == nil {
path: deployment/common/changeset/internal/solana/access_controller.go
user: login: gustavogama-cll
body: 1 and 3 can go together. Erroring out is ok as well, and it's what I'd expect. Manually editing the address book shouldn't happen often anyways. And there's a lot more that can break if someone edits it in unexpected ways.

I pushed a recent update with 1 + 3 -- implemented consistently for all three contract types. If you feel like 2 is the way to go, just replace the `Logger.Warn` with `return fmt.Error("...")`.
created_at: 2025-03-20T17:20:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: cl-sonarqube-production[bot]
created_at: 2025-03-20T17:38:30Z
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_chainlink&pullRequest=16867) **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_chainlink&pullRequest=16867&resolved=false) [0 New issues](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_chainlink&pullRequest=16867&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_chainlink&fixedInPullRequest=16867) [0 Fixed issues](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_chainlink&fixedInPullRequest=16867)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_chainlink&pullRequest=16867&issueStatuses=ACCEPTED) [0 Accepted issues](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_chainlink&pullRequest=16867&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_chainlink&pullRequest=16867) [0 Security Hotspots](https://sonarqube.main.prod.cldev.sh/security_hotspots?id=smartcontractkit_chainlink&pullRequest=16867)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_chainlink&pullRequest=16867&metric=new_coverage&view=list) [0.0% Coverage on New Code](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_chainlink&pullRequest=16867&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_chainlink&pullRequest=16867&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_chainlink&pullRequest=16867&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://sonarqube.main.prod.cldev.sh/dashboard?id=smartcontractkit_chainlink&pullRequest=16867)


author_association: NONE