|
| 1 | +package legacy |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "slices" |
| 7 | + "strings" |
| 8 | + "sync" |
| 9 | + |
| 10 | + "github.com/ethereum/go-ethereum/accounts/abi/bind" |
| 11 | + mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms" |
| 12 | + xerrgroup "golang.org/x/sync/errgroup" |
| 13 | + |
| 14 | + evmchangesets "github.com/smartcontractkit/cld-changesets/pkg/family/evm/changesets" |
| 15 | + evmstate "github.com/smartcontractkit/cld-changesets/pkg/family/evm/legacy" |
| 16 | + |
| 17 | + opsevm "github.com/smartcontractkit/cld-changesets/pkg/family/evm/operations" |
| 18 | + solchangesets "github.com/smartcontractkit/cld-changesets/pkg/family/solana/changesets/legacy" |
| 19 | + |
| 20 | + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" |
| 21 | + |
| 22 | + "github.com/ethereum/go-ethereum/common" |
| 23 | + "github.com/gagliardetto/solana-go" |
| 24 | + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" |
| 25 | + chainselectors "github.com/smartcontractkit/chain-selectors" |
| 26 | + |
| 27 | + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" |
| 28 | + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" |
| 29 | + |
| 30 | + "github.com/smartcontractkit/chainlink-deployments-framework/operations" |
| 31 | +) |
| 32 | + |
| 33 | +// migrateAddressBookWithQualifiers migrates an address book to a data store, |
| 34 | +// applying custom qualifiers from MCMS configs when available |
| 35 | +func migrateAddressBookWithQualifiers(ab cldf.AddressBook, cfgByChain map[uint64]cldfproposalutils.MCMSWithTimelockConfig) (datastore.MutableDataStore, error) { |
| 36 | + addrs, err := ab.Addresses() |
| 37 | + if err != nil { |
| 38 | + return nil, err |
| 39 | + } |
| 40 | + |
| 41 | + ds := datastore.NewMemoryDataStore() |
| 42 | + |
| 43 | + for chainSelector, chainAddresses := range addrs { |
| 44 | + // Get the qualifier for this chain from the config |
| 45 | + qualifier := "" |
| 46 | + if cfg, exists := cfgByChain[chainSelector]; exists && cfg.Qualifier != nil && *cfg.Qualifier != "" { |
| 47 | + qualifier = *cfg.Qualifier |
| 48 | + } |
| 49 | + |
| 50 | + for addr, typever := range chainAddresses { |
| 51 | + ref := datastore.AddressRef{ |
| 52 | + ChainSelector: chainSelector, |
| 53 | + Address: addr, |
| 54 | + Type: datastore.ContractType(typever.Type), |
| 55 | + Version: &typever.Version, |
| 56 | + } |
| 57 | + |
| 58 | + // If we have a custom qualifier for this chain, use it for MCMS contracts |
| 59 | + if qualifier != "" && isMCMSContract(string(typever.Type)) { |
| 60 | + ref.Qualifier = qualifier |
| 61 | + } |
| 62 | + |
| 63 | + // If the address book has labels, we need to add them to the addressRef |
| 64 | + if !typever.Labels.IsEmpty() { |
| 65 | + ref.Labels = datastore.NewLabelSet(typever.Labels.List()...) |
| 66 | + } |
| 67 | + |
| 68 | + if err = ds.Addresses().Add(ref); err != nil { |
| 69 | + return nil, fmt.Errorf("failed to add address %s: %w", addr, err) |
| 70 | + } |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + return ds, nil |
| 75 | +} |
| 76 | + |
| 77 | +// isMCMSContract checks if a contract type is part of the MCMS system |
| 78 | +func isMCMSContract(contractType string) bool { |
| 79 | + mcmsTypes := []string{ |
| 80 | + string(mcmscontracts.RBACTimelock), |
| 81 | + string(mcmscontracts.ManyChainMultisig), |
| 82 | + string(mcmscontracts.ProposerManyChainMultisig), |
| 83 | + string(mcmscontracts.BypasserManyChainMultisig), |
| 84 | + string(mcmscontracts.CancellerManyChainMultisig), |
| 85 | + string(mcmscontracts.CallProxy), |
| 86 | + } |
| 87 | + |
| 88 | + return slices.Contains(mcmsTypes, contractType) |
| 89 | +} |
| 90 | + |
| 91 | +var ( |
| 92 | + _ cldf.ChangeSet[map[uint64]cldfproposalutils.MCMSWithTimelockConfig] = DeployMCMSWithTimelockV2 |
| 93 | + |
| 94 | + // GrantRoleInTimeLock grants proposer, canceller, bypasser, executor, admin roles to the timelock contract with corresponding addresses if the |
| 95 | + // roles are not already set with the same addresses. |
| 96 | + // It creates a proposal if deployer key is not admin of the timelock contract. |
| 97 | + // otherwise it executes the transactions directly. |
| 98 | + // If neither timelock, nor the deployer key is the admin of the timelock contract, it returns an error. |
| 99 | + GrantRoleInTimeLock = cldf.CreateChangeSet(grantRoleLogic, grantRolePreconditions) |
| 100 | +) |
| 101 | + |
| 102 | +// DeployMCMSWithTimelockV2 deploys and initializes the MCM and Timelock contracts |
| 103 | +func DeployMCMSWithTimelockV2( |
| 104 | + env cldf.Environment, cfgByChain map[uint64]cldfproposalutils.MCMSWithTimelockConfig, |
| 105 | +) (cldf.ChangesetOutput, error) { |
| 106 | + newAddresses := cldf.NewMemoryAddressBook() |
| 107 | + |
| 108 | + eg := xerrgroup.Group{} |
| 109 | + mu := sync.Mutex{} |
| 110 | + allReports := make([]operations.Report[any, any], 0) |
| 111 | + for chainSel, cfg := range cfgByChain { |
| 112 | + eg.Go(func() error { |
| 113 | + family, err := chainselectors.GetSelectorFamily(chainSel) |
| 114 | + if err != nil { |
| 115 | + return err |
| 116 | + } |
| 117 | + |
| 118 | + switch family { |
| 119 | + case chainselectors.FamilyEVM: |
| 120 | + // Extract qualifier from config for this chain |
| 121 | + qualifier := "" |
| 122 | + if cfg.Qualifier != nil { |
| 123 | + qualifier = *cfg.Qualifier |
| 124 | + } |
| 125 | + |
| 126 | + // load mcms state with qualifier awareness |
| 127 | + // we load the state one by one to avoid early return from MaybeLoadMCMSWithTimelockStateWithQualifier |
| 128 | + // due to one of the chain not found |
| 129 | + var chainstate *evmstate.MCMSWithTimelockState |
| 130 | + s, err := evmstate.MaybeLoadMCMSWithTimelockStateWithQualifier(env, []uint64{chainSel}, qualifier) |
| 131 | + if err != nil { |
| 132 | + // if the state is not found for chain, we assume it's a fresh deployment |
| 133 | + // this includes "no addresses found" which is expected for new qualifiers |
| 134 | + if !strings.Contains(err.Error(), cldf.ErrChainNotFound.Error()) && |
| 135 | + !strings.Contains(err.Error(), "no addresses found") { |
| 136 | + return err |
| 137 | + } |
| 138 | + } |
| 139 | + if s != nil { |
| 140 | + chainstate = s[chainSel] |
| 141 | + } |
| 142 | + reports, err := evmchangesets.DeployMCMSWithTimelockContractsEVM(env, env.BlockChains.EVMChains()[chainSel], newAddresses, cfg, chainstate) |
| 143 | + mu.Lock() |
| 144 | + allReports = append(allReports, reports...) |
| 145 | + mu.Unlock() |
| 146 | + |
| 147 | + return err |
| 148 | + |
| 149 | + case chainselectors.FamilySolana: |
| 150 | + // this is not used in CLD as we need to dynamically resolve the artifacts to deploy these contracts |
| 151 | + // we did not want to add the artifact resolution logic here, so we instead deploy using ccip/changeset/solana/cs_deploy_chain.go |
| 152 | + // for in memory tests, programs and state are pre-loaded, so we use this function via testhelpers.TransferOwnershipSolana |
| 153 | + _, err := solchangesets.DeployMCMSWithTimelockProgramsSolana(env, env.BlockChains.SolanaChains()[chainSel], newAddresses, cfg) |
| 154 | + return err |
| 155 | + |
| 156 | + default: |
| 157 | + return fmt.Errorf("unsupported chain family: %s", family) |
| 158 | + } |
| 159 | + }) |
| 160 | + } |
| 161 | + err := eg.Wait() |
| 162 | + if err != nil { |
| 163 | + return cldf.ChangesetOutput{Reports: allReports, AddressBook: newAddresses}, err |
| 164 | + } |
| 165 | + ds, err := migrateAddressBookWithQualifiers(newAddresses, cfgByChain) |
| 166 | + if err != nil { |
| 167 | + return cldf.ChangesetOutput{Reports: allReports, AddressBook: newAddresses}, fmt.Errorf("failed to migrate address book to data store: %w", err) |
| 168 | + } |
| 169 | + |
| 170 | + return cldf.ChangesetOutput{Reports: allReports, AddressBook: newAddresses, DataStore: ds}, nil |
| 171 | +} |
| 172 | + |
| 173 | +type GrantRoleInput struct { |
| 174 | + ExistingProposerByChain map[uint64]common.Address // if needed in the future, need to add bypasser and canceller here |
| 175 | + MCMS *cldfproposalutils.TimelockConfig |
| 176 | + GasBoostConfigPerChain map[uint64]cldfproposalutils.GasBoostConfig |
| 177 | +} |
| 178 | + |
| 179 | +func grantRolePreconditions(e cldf.Environment, cfg GrantRoleInput) error { |
| 180 | + mcmsState, err := loadMCMSStatePerChainWithQualifier(e, cfg) |
| 181 | + if err != nil { |
| 182 | + return err |
| 183 | + } |
| 184 | + for selector, proposer := range cfg.ExistingProposerByChain { |
| 185 | + if proposer == (common.Address{}) { |
| 186 | + return fmt.Errorf("proposer address not found for chain %d", selector) |
| 187 | + } |
| 188 | + chain, ok := e.BlockChains.EVMChains()[selector] |
| 189 | + if !ok { |
| 190 | + return fmt.Errorf("chain not found for chain %d", selector) |
| 191 | + } |
| 192 | + timelockContracts, ok := mcmsState[selector] |
| 193 | + if !ok { |
| 194 | + return fmt.Errorf("timelock state not found for chain %d", selector) |
| 195 | + } |
| 196 | + if timelockContracts.Timelock == nil { |
| 197 | + return fmt.Errorf("timelock contract not found for chain %s", chain.String()) |
| 198 | + } |
| 199 | + if timelockContracts.ProposerMcm == nil { |
| 200 | + return fmt.Errorf("proposerMcm contract not found for chain %s", chain.String()) |
| 201 | + } |
| 202 | + if timelockContracts.CancellerMcm == nil { |
| 203 | + return fmt.Errorf("cancellerMcm contract not found for chain %s", chain.String()) |
| 204 | + } |
| 205 | + if timelockContracts.BypasserMcm == nil { |
| 206 | + return fmt.Errorf("bypasserMcm contract not found for chain %s", chain.String()) |
| 207 | + } |
| 208 | + if timelockContracts.CallProxy == nil { |
| 209 | + return fmt.Errorf("callProxy contract not found for chain %s", chain.String()) |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + return nil |
| 214 | +} |
| 215 | + |
| 216 | +// loads MCMS state for each chain using per-chain qualifiers from cfg.MCMS.TimelockQualifierPerChain when available |
| 217 | +func loadMCMSStatePerChainWithQualifier(e cldf.Environment, cfg GrantRoleInput) (map[uint64]*evmstate.MCMSWithTimelockState, error) { |
| 218 | + result := make(map[uint64]*evmstate.MCMSWithTimelockState) |
| 219 | + for selector := range cfg.ExistingProposerByChain { |
| 220 | + qualifier := "" |
| 221 | + if cfg.MCMS != nil && cfg.MCMS.TimelockQualifierPerChain != nil { |
| 222 | + qualifier = cfg.MCMS.TimelockQualifierPerChain[selector] |
| 223 | + } |
| 224 | + chainState, err := evmstate.MaybeLoadMCMSWithTimelockStateWithQualifier(e, []uint64{selector}, qualifier) |
| 225 | + if err != nil { |
| 226 | + return nil, err |
| 227 | + } |
| 228 | + result[selector] = chainState[selector] |
| 229 | + } |
| 230 | + |
| 231 | + return result, nil |
| 232 | +} |
| 233 | + |
| 234 | +func grantRoleLogic(e cldf.Environment, cfg GrantRoleInput) (cldf.ChangesetOutput, error) { |
| 235 | + mcmsState, err := loadMCMSStatePerChainWithQualifier(e, cfg) |
| 236 | + if err != nil { |
| 237 | + return cldf.ChangesetOutput{}, err |
| 238 | + } |
| 239 | + mcmsStateForProposal := make(map[uint64]evmstate.MCMSWithTimelockState) |
| 240 | + for k, v := range mcmsState { |
| 241 | + if v != nil { |
| 242 | + // Replace the proposer MCM in state with the existing proposer. |
| 243 | + // This is to ensure that we are using an MCM contract that already has the proposer role. |
| 244 | + existingProposerMcm, err := gethwrappers.NewManyChainMultiSig( |
| 245 | + cfg.ExistingProposerByChain[k], |
| 246 | + e.BlockChains.EVMChains()[k].Client, |
| 247 | + ) |
| 248 | + if err != nil { |
| 249 | + return cldf.ChangesetOutput{}, fmt.Errorf("failed to create ManyChainMultiSig for existing proposer %s on chain %d: %w", |
| 250 | + cfg.ExistingProposerByChain[k].Hex(), k, err) |
| 251 | + } |
| 252 | + mcmsStateForProposal[k] = evmstate.MCMSWithTimelockState{ |
| 253 | + CancellerMcm: v.CancellerMcm, |
| 254 | + BypasserMcm: v.BypasserMcm, |
| 255 | + ProposerMcm: existingProposerMcm, |
| 256 | + Timelock: v.Timelock, |
| 257 | + CallProxy: v.CallProxy, |
| 258 | + } |
| 259 | + } |
| 260 | + } |
| 261 | + |
| 262 | + out := cldf.ChangesetOutput{} |
| 263 | + gasBoostConfigs := opsevm.GasBoostConfigsForChainMap(cfg.ExistingProposerByChain, cfg.GasBoostConfigPerChain) |
| 264 | + for chain := range cfg.ExistingProposerByChain { |
| 265 | + stateForChain := mcmsState[chain] |
| 266 | + evmChains := e.BlockChains.EVMChains() |
| 267 | + seqReport, err := evmchangesets.GrantRolesForTimelock( |
| 268 | + e, evmChains[chain], &cldfproposalutils.MCMSWithTimelockContracts{ |
| 269 | + CancellerMcm: stateForChain.CancellerMcm, |
| 270 | + BypasserMcm: stateForChain.BypasserMcm, |
| 271 | + ProposerMcm: stateForChain.ProposerMcm, |
| 272 | + Timelock: stateForChain.Timelock, |
| 273 | + CallProxy: stateForChain.CallProxy, |
| 274 | + }, false, gasBoostConfigs[chain]) |
| 275 | + out, err = opsevm.AddEVMCallSequenceToCSOutput(e, out, seqReport, err, mcmsStateForProposal, cfg.MCMS, fmt.Sprintf("GrantRolesForTimelock on %s", evmChains[chain])) |
| 276 | + if err != nil { |
| 277 | + return out, fmt.Errorf("failed to grant roles for timelock on chain %d: %w", chain, err) |
| 278 | + } |
| 279 | + } |
| 280 | + |
| 281 | + return out, nil |
| 282 | +} |
| 283 | + |
| 284 | +func ValidateOwnership(ctx context.Context, mcms bool, deployerKey, timelock common.Address, contract evmstate.Ownable) error { |
| 285 | + owner, err := contract.Owner(&bind.CallOpts{Context: ctx}) |
| 286 | + if err != nil { |
| 287 | + return fmt.Errorf("failed to get owner: %w", err) |
| 288 | + } |
| 289 | + if mcms && owner != timelock { |
| 290 | + return fmt.Errorf("%s not owned by timelock, Owner: %s", contract.Address(), owner.Hex()) |
| 291 | + } else if !mcms && owner != deployerKey { |
| 292 | + return fmt.Errorf("%s not owned by deployer key, Owner: %s", contract.Address(), owner.Hex()) |
| 293 | + } |
| 294 | + |
| 295 | + return nil |
| 296 | +} |
| 297 | + |
| 298 | +func ValidateOwnershipSolanaCommon(mcms bool, deployerKey solana.PublicKey, timelockSignerPDA solana.PublicKey, programOwner solana.PublicKey) error { |
| 299 | + if !mcms { |
| 300 | + if deployerKey.String() != programOwner.String() { |
| 301 | + return fmt.Errorf("deployer key %s does not match owner %s", deployerKey.String(), programOwner.String()) |
| 302 | + } |
| 303 | + } else { |
| 304 | + if timelockSignerPDA.String() != programOwner.String() { |
| 305 | + return fmt.Errorf("timelock signer PDA %s does not match owner %s", timelockSignerPDA.String(), programOwner.String()) |
| 306 | + } |
| 307 | + } |
| 308 | + |
| 309 | + return nil |
| 310 | +} |
0 commit comments