Skip to content

Commit b05953d

Browse files
committed
fix: add checks for duplicate timelock refs or conflicting regs with mcms
1 parent 5d0d941 commit b05953d

4 files changed

Lines changed: 51 additions & 32 deletions

File tree

mcms/changesets/deploy-custom-topology/changeset.go

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
1212
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
1313
mcmstypes "github.com/smartcontractkit/mcms/types"
14+
15+
"github.com/smartcontractkit/cld-changesets/internal/maputil"
1416
)
1517

1618
var _ cldf.ChangeSetV2[Input] = Changeset{}
@@ -40,7 +42,7 @@ func (Changeset) Apply(e cldf.Environment, input Input) (cldf.ChangesetOutput, e
4042
var agg ChainOutput
4143
var reports []operations.Report[any, any]
4244

43-
for _, chainSelector := range sortedChainSelectors(input.Cfg.ChainConfigs) {
45+
for _, chainSelector := range maputil.SortedMapKeys(input.Cfg.ChainConfigs) {
4446
seq, seqErr := SequenceForChainSelector(chainSelector)
4547
if seqErr != nil {
4648
return buildOutput(e, input.MCMS, agg, reports, fmt.Errorf("chain selector %d: %w", chainSelector, seqErr))
@@ -140,7 +142,7 @@ func validateConfig(e cldf.Environment, cfg Config, mcms *cldf.MCMSTimelockPropo
140142
}
141143

142144
byFamily := make(map[string][]ChainInput)
143-
for _, chainSelector := range sortedChainSelectors(cfg.ChainConfigs) {
145+
for _, chainSelector := range maputil.SortedMapKeys(cfg.ChainConfigs) {
144146
chainCfg := cfg.ChainConfigs[chainSelector]
145147
family, err := chain_selectors.GetSelectorFamily(chainSelector)
146148
if err != nil {
@@ -173,29 +175,39 @@ func validateConfig(e cldf.Environment, cfg Config, mcms *cldf.MCMSTimelockPropo
173175
return nil
174176
}
175177

176-
// validateChainConfig checks a single chain's topology: unique MCM refs, every
177-
// referenced MCMRef is declared, every RoleHolder is well-formed, and ownership
178-
// transfers require an MCMS input.
178+
// validateChainConfig checks a single chain's topology: unique refs within MCMs
179+
// and timelocks (with no overlap between the two), every referenced MCMRef is
180+
// declared, every RoleHolder is well-formed, and ownership transfers require an
181+
// MCMS input.
179182
func validateChainConfig(chainSelector uint64, cfg ChainTopologyConfig, mcms *cldf.MCMSTimelockProposalInput) error {
180183
if len(cfg.MCMs) == 0 && len(cfg.Timelocks) == 0 {
181184
return fmt.Errorf("chain %d: no MCMs or timelocks to deploy", chainSelector)
182185
}
183186

184-
declared := make(map[string]struct{}, len(cfg.MCMs))
187+
mcmRefs := make(map[string]struct{}, len(cfg.MCMs))
185188
for i, m := range cfg.MCMs {
186189
if m.Ref == "" {
187190
return fmt.Errorf("chain %d: mcms[%d]: ref is required", chainSelector, i)
188191
}
189-
if _, dup := declared[m.Ref]; dup {
192+
if _, dup := mcmRefs[m.Ref]; dup {
190193
return fmt.Errorf("chain %d: duplicate MCM ref %q", chainSelector, m.Ref)
191194
}
192-
declared[m.Ref] = struct{}{}
195+
mcmRefs[m.Ref] = struct{}{}
193196
}
194197

198+
timelockRefs := make(map[string]struct{}, len(cfg.Timelocks))
195199
for i, t := range cfg.Timelocks {
196200
if t.Ref == "" {
197201
return fmt.Errorf("chain %d: timelocks[%d]: ref is required", chainSelector, i)
198202
}
203+
if _, dup := timelockRefs[t.Ref]; dup {
204+
return fmt.Errorf("chain %d: duplicate timelock ref %q", chainSelector, t.Ref)
205+
}
206+
if _, collides := mcmRefs[t.Ref]; collides {
207+
return fmt.Errorf("chain %d: timelock ref %q conflicts with an MCM ref", chainSelector, t.Ref)
208+
}
209+
timelockRefs[t.Ref] = struct{}{}
210+
199211
if t.Qualifier == "" {
200212
return fmt.Errorf("chain %d: timelock %q: qualifier is required", chainSelector, t.Ref)
201213
}
@@ -207,11 +219,11 @@ func validateChainConfig(chainSelector uint64, cfg ChainTopologyConfig, mcms *cl
207219
"admins": t.Roles.Admins,
208220
}
209221
for name, holders := range roleSets {
210-
if err := validateRoleHolders(chainSelector, t.Ref, name, holders, declared); err != nil {
222+
if err := validateRoleHolders(chainSelector, t.Ref, name, holders, mcmRefs); err != nil {
211223
return err
212224
}
213225
}
214-
if err := validateRoleHolders(chainSelector, t.Ref, "transferOwnership", t.TransferOwnership, declared); err != nil {
226+
if err := validateRoleHolders(chainSelector, t.Ref, "transferOwnership", t.TransferOwnership, mcmRefs); err != nil {
215227
return err
216228
}
217229
if len(t.TransferOwnership) > 0 && mcms == nil {
@@ -239,16 +251,6 @@ func validateRoleHolders(chainSelector uint64, timelockRef, field string, holder
239251
return nil
240252
}
241253

242-
func sortedChainSelectors(byChain map[uint64]ChainTopologyConfig) []uint64 {
243-
selectors := make([]uint64, 0, len(byChain))
244-
for chainSelector := range byChain {
245-
selectors = append(selectors, chainSelector)
246-
}
247-
slices.Sort(selectors)
248-
249-
return selectors
250-
}
251-
252254
// mergeChainOutputs concatenates metadata and proposal groups from one chain's
253255
// sequence output into the running aggregate.
254256
func mergeChainOutputs(agg, out ChainOutput) ChainOutput {

mcms/changesets/deploy-custom-topology/changeset_test.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ func TestChangeset_VerifyPreconditions(t *testing.T) {
113113
dupMCM := validChainConfig()
114114
dupMCM.MCMs = append(dupMCM.MCMs, MCMSpec{Ref: "proposer", Qualifier: "CCIP"})
115115

116+
dupTimelock := validChainConfig()
117+
dupTimelock.Timelocks = append(dupTimelock.Timelocks, TimelockSpec{
118+
Ref: "timelock",
119+
MinDelay: big.NewInt(0),
120+
Qualifier: "RMN",
121+
})
122+
123+
mcmTimelockRefCollision := validChainConfig()
124+
mcmTimelockRefCollision.Timelocks[0].Ref = "proposer"
125+
116126
undeclaredRef := validChainConfig()
117127
undeclaredRef.Timelocks[0].Roles.Proposers = []RoleHolder{{MCMRef: "missing"}}
118128

@@ -150,6 +160,16 @@ func TestChangeset_VerifyPreconditions(t *testing.T) {
150160
input: Input{Cfg: Config{ChainConfigs: map[uint64]ChainTopologyConfig{fakeEVMFamilySelector: dupMCM}}},
151161
wantErr: "duplicate MCM ref",
152162
},
163+
{
164+
name: "duplicate timelock ref",
165+
input: Input{Cfg: Config{ChainConfigs: map[uint64]ChainTopologyConfig{fakeEVMFamilySelector: dupTimelock}}},
166+
wantErr: "duplicate timelock ref",
167+
},
168+
{
169+
name: "timelock ref conflicts with MCM ref",
170+
input: Input{Cfg: Config{ChainConfigs: map[uint64]ChainTopologyConfig{fakeEVMFamilySelector: mcmTimelockRefCollision}}},
171+
wantErr: "conflicts with an MCM ref",
172+
},
153173
{
154174
name: "undeclared mcmRef",
155175
input: Input{Cfg: Config{ChainConfigs: map[uint64]ChainTopologyConfig{fakeEVMFamilySelector: undeclaredRef}}},
@@ -256,13 +276,6 @@ func TestChangeset_Apply_unregisteredFamilyErrors(t *testing.T) {
256276
require.ErrorContains(t, err, "chain selector 1")
257277
}
258278

259-
func TestSortedChainSelectors(t *testing.T) {
260-
t.Parallel()
261-
262-
got := sortedChainSelectors(map[uint64]ChainTopologyConfig{3: {}, 1: {}, 2: {}})
263-
require.Equal(t, []uint64{1, 2, 3}, got)
264-
}
265-
266279
func TestMergeChainOutputs(t *testing.T) {
267280
t.Parallel()
268281

mcms/changesets/deploy-custom-topology/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ type MCMSpec struct {
5959
// TimelockSpec is one timelock (RBACTimelock on EVM) to deploy, with its role
6060
// wiring and optional ownership transfers.
6161
type TimelockSpec struct {
62+
// Ref is a logical handle, unique within the chain and distinct from every
63+
// MCMSpec.Ref on the same chain.
6264
Ref string `json:"ref"`
6365

6466
MinDelay *big.Int `json:"minDelay"`

mcms/evm/deploy-custom-topology/sequence.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ func runEVMDeployTopology(
8282
if err != nil {
8383
return out, err
8484
}
85-
8685
}
8786

8887
return out, nil
@@ -166,6 +165,7 @@ func deployCallProxy(
166165
return common.Address{}, fmt.Errorf("deploy call proxy for timelock %q on chain %d: %w", timelockSpec.Ref, chainSelector, err)
167166
}
168167
callProxyAddr := common.HexToAddress(cpReport.Output.Address)
168+
169169
return callProxyAddr, nil
170170
}
171171

@@ -191,15 +191,15 @@ func deployTimelockAndSetRoles(
191191
}
192192
bypassers, err := resolveHolders(tl.Roles.Bypassers, refToAddr)
193193
if err != nil {
194-
fmt.Errorf("timelock %q bypassers: %w", tl.Ref, err)
194+
return fmt.Errorf("timelock %q bypassers: %w", tl.Ref, err)
195195
}
196196
executors, err := resolveHolders(tl.Roles.Executors, refToAddr)
197197
if err != nil {
198-
fmt.Errorf("timelock %q executors: %w", tl.Ref, err)
198+
return fmt.Errorf("timelock %q executors: %w", tl.Ref, err)
199199
}
200200
admins, err := resolveHolders(tl.Roles.Admins, refToAddr)
201201
if err != nil {
202-
fmt.Errorf("timelock %q admins: %w", tl.Ref, err)
202+
return fmt.Errorf("timelock %q admins: %w", tl.Ref, err)
203203
}
204204

205205
// Deploy the timelock with the deployer as initial admin. Executors are left
@@ -225,7 +225,7 @@ func deployTimelockAndSetRoles(
225225
chainIdempotencyKey[opscontract.DeployInput[timelockops.ConstructorArgs], cldf_evm.Chain](chain),
226226
)
227227
if err != nil {
228-
fmt.Errorf("deploy timelock %q on chain %d: %w", tl.Ref, chainSelector, err)
228+
return fmt.Errorf("deploy timelock %q on chain %d: %w", tl.Ref, chainSelector, err)
229229
}
230230
timelockAddr := common.HexToAddress(tlReport.Output.Address)
231231
refToAddr[tl.Ref] = timelockAddr
@@ -348,6 +348,7 @@ func grantRoles(
348348
"Role", g.Name,
349349
"Error", err,
350350
)
351+
351352
return err
352353
}
353354

@@ -375,6 +376,7 @@ func grantRoles(
375376
"Role Name", g.Name,
376377
"Address", addr.Hex(),
377378
)
379+
378380
return err
379381
}
380382
b.Logger.Infow("Role granted",

0 commit comments

Comments
 (0)