Skip to content

Commit 39a62f2

Browse files
authored
feat: port deploy mcms with timelock (#28)
* Port all the code related to [deployment/common/changeset/deploy_mcms_with_timelock.go](https://github.com/smartcontractkit/chainlink/blob/develop/deployment/common/changeset/deploy_mcms_with_timelock.go) from core repo to cld-changesets. * create `legacy` pkgs for code that will be deprecated when refactor of changesets is done. ## AI Summary This pull request updates the project's dependencies in the `go.mod` file. The main changes include upgrading the Go version, updating several direct and indirect dependencies to newer versions, adding new dependencies, and replacing a module with a local path. These updates help keep the codebase up-to-date with the latest features, security patches, and compatibility improvements. **Dependency and Version Updates:** - Upgraded the Go version from `1.25.7` to `1.26.2` in `go.mod`. - Updated several key dependencies to newer versions, such as `github.com/aptos-labs/aptos-go-sdk`, `github.com/deckarep/golang-set/v2`, `github.com/ethereum/go-ethereum`, `github.com/smartcontractkit/chain-selectors`, `github.com/smartcontractkit/chainlink-ccip/chains/solana`, `github.com/smartcontractkit/chainlink-common`, and many others. - Added many new indirect dependencies, including libraries for AWS SDK, Cosmos SDK, Solana, Gin, and others, expanding the project's capabilities and integrations. [[1]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6L3-R203) [[2]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6R215-R269) [[3]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6R279-R315) [[4]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6L178-R348) **Module Replacement and Additions:** - Replaced the `github.com/smartcontractkit/chainlink-deployments-framework` module with a local path to `../chainlink-deployments-framework`, likely to use a local development version. - Added new direct dependencies such as `github.com/smartcontractkit/chainlink-ton/deployment`, `github.com/smartcontractkit/chainlink/deployment`, `github.com/smartcontractkit/quarantine`, and `github.com/smartcontractkit/wsrpc`. **General Maintenance:** - Updated and added a large number of indirect dependencies for testing, serialization, cryptography, cloud integrations, and more, ensuring the project remains compatible with the latest ecosystem tools. [[1]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6R215-R269) [[2]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6R279-R315) [[3]](diffhunk://#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6L178-R348) These changes collectively help maintain and improve the stability, security, and feature set of the project.
1 parent c22ad31 commit 39a62f2

62 files changed

Lines changed: 5864 additions & 485 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
golang 1.25.7
1+
golang 1.26.2
22
golangci-lint 2.11.4
33
mockery 3.6.3
44
task 3.48.0

go.mod

Lines changed: 88 additions & 80 deletions
Large diffs are not rendered by default.

go.sum

Lines changed: 542 additions & 252 deletions
Large diffs are not rendered by default.

link/changesets/deploy_link_token_test.go

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime"
1818

1919
cldchangesetscommon "github.com/smartcontractkit/cld-changesets/pkg/common"
20-
evmstate "github.com/smartcontractkit/cld-changesets/pkg/family/evm"
20+
evmstate "github.com/smartcontractkit/cld-changesets/pkg/family/evm/legacy"
2121
)
2222

2323
func TestDeployLinkToken(t *testing.T) {
@@ -138,19 +138,6 @@ func TestDeployLinkTokenRejectsExistingStateBeforeDeploy(t *testing.T) {
138138
},
139139
wantErr: "LinkToken contract already exists",
140140
},
141-
{
142-
name: "link token exists in datastore with nil version",
143-
env: cldf.Environment{
144-
BlockChains: cldf_chain.NewBlockChainsFromSlice([]cldf_chain.BlockChain{
145-
cldf_evm.Chain{Selector: evmSelector},
146-
}),
147-
DataStore: datastoreWithNilVersion(t, evmSelector, evmAddress, linkcontracts.LinkToken, "migrated"),
148-
},
149-
run: func(env cldf.Environment) (cldf.ChangesetOutput, error) {
150-
return DeployLinkToken(env, []uint64{evmSelector})
151-
},
152-
wantErr: "LinkToken contract already exists",
153-
},
154141
{
155142
name: "static link token exists in datastore",
156143
env: cldf.Environment{
@@ -239,20 +226,6 @@ func datastoreWith(t *testing.T, selector uint64, address string, tv cldf.TypeAn
239226
return ds.Seal()
240227
}
241228

242-
func datastoreWithNilVersion(t *testing.T, selector uint64, address string, contractType cldf.ContractType, qualifier string) datastore.DataStore {
243-
t.Helper()
244-
245-
ds := datastore.NewMemoryDataStore()
246-
require.NoError(t, ds.Addresses().Add(datastore.AddressRef{
247-
ChainSelector: selector,
248-
Address: address,
249-
Type: datastore.ContractType(contractType.String()),
250-
Qualifier: qualifier,
251-
}))
252-
253-
return ds.Seal()
254-
}
255-
256229
func typeAndVersionWithLabels(tv cldf.TypeAndVersion, labels ...string) cldf.TypeAndVersion {
257230
for _, label := range labels {
258231
tv.Labels.Add(label)
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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

Comments
 (0)