Documentation
¶
Overview ¶
Package deploycustomtopology provides the DeployCustomTopology changeset and a registry for per-chain-family deploy implementations. It deploys an arbitrary MCMS topology per chain — any number of MCMs and timelocks, custom role wiring, and optional ownership transfers — unlike the fixed standard-topology deploy.
Usage ¶
Import the changeset and blank-import each chain family you use (plus MCMS readers, required when ownership transfers build timelock proposals). This follows the Go plugin pattern used by database/sql drivers:
import ( topology "github.com/smartcontractkit/cld-changesets/mcms/changesets/deploy-custom-topology" _ "github.com/smartcontractkit/cld-changesets/mcms/evm/readers" _ "github.com/smartcontractkit/cld-changesets/mcms/evm/deploy-custom-topology" )
For pipelines that need every built-in family, blank-import [all] instead:
_ "github.com/smartcontractkit/cld-changesets/mcms/changesets/deploy-custom-topology/all"
Example: one proposer MCM and one timelock ¶
MCMSTopologyConfig.ChainConfigs maps chain selectors to the topology to deploy on that chain. Each chain declares any number of MCMSpec entries (the MCMs to deploy) and TimelockSpec entries (the timelocks to deploy). Ref fields are local handles used within that chain; RoleHolder.MCMRef must match an MCMSpec.Ref on the same chain.
Minimal example — one proposer MCM wired as the sole proposer on one timelock:
rt.Exec(runtime.ChangesetTask(topology.Changeset{}, topology.Input{
Cfg: topology.MCMSTopologyConfig{
ChainConfigs: map[uint64]topology.ChainTopologyConfig{
selector: {
MCMs: []topology.MCMSpec{{
Ref: "proposer",
Qualifier: "CCIP",
ContractType: "ProposerManyChainMultisig",
Config: proposerCfg, // mcmstypes.Config: signers + quorum
}},
Timelocks: []topology.TimelockSpec{{
Ref: "timelock",
Qualifier: "CCIP",
MinDelay: big.NewInt(3600),
Roles: topology.RoleAssignments{
Proposers: []topology.RoleHolder{{MCMRef: "proposer"}},
},
}},
},
},
},
}))
// CLD:
registry.Add("deploy_custom_topology", Configure(topology.Changeset{}).WithEnvInput())
Multiple MCMs on the same timelock — add more MCMSpec entries and reference them from TimelockSpec.Roles:
MCMs: []topology.MCMSpec{
{Ref: "proposer", Qualifier: "CCIP", ContractType: "ProposerManyChainMultisig", Config: proposerCfg},
{Ref: "bypasser", Qualifier: "CCIP", ContractType: "BypasserManyChainMultisig", Config: bypasserCfg},
},
Timelocks: []topology.TimelockSpec{{
Ref: "timelock", Qualifier: "CCIP", MinDelay: big.NewInt(3600),
Roles: topology.RoleAssignments{
Proposers: []topology.RoleHolder{{MCMRef: "proposer"}},
Bypassers: []topology.RoleHolder{{MCMRef: "bypasser"}},
Cancellers: []topology.RoleHolder{{MCMRef: "proposer"}, {MCMRef: "bypasser"}},
},
}},
Leave [Input.MCMS] nil for deploy-only runs. Set TimelockSpec.TransferOwnership to move ownership onto a timelock: the changeset executes transferOwnership on-chain and returns accept-ownership timelock proposals grouped by timelock qualifier. Duplicate targets (same resolved address) are deduplicated silently. Leave [Input.MCMS].Qualifier empty — it is taken from TimelockSpec.Qualifier when building one proposal per qualifier.
See mcms/evm/deploy-custom-topology/sequence_test.go for full EVM examples, including the standard three-MCM topology and multi-qualifier setups.
Behaviour ¶
Changeset iterates the configured chains, runs each chain's registered deploy sequence, writes deployed-address metadata to the changeset datastore, and — when [Input.MCMS] is set and a timelock takes ownership of contracts — builds one MCMS timelock proposal per timelock qualifier from the returned batch operations.
Each family sequence returns sequenceutils.OnChainOutput. Populate Metadata with deployed addresses and BatchOps with accept-ownership batch operations (only when running with MCMS input). The changeset groups batch operations by timelock qualifier when building proposals.
Built-in EVM and Solana support ¶
EVM and Solana register themselves via init when their packages are imported. Importing this package alone does not register any family — you must blank-import mcms/<family>/deploy-custom-topology (and mcms/<family>/readers for MCMS proposals).
Adding a new chain family ¶
Implement a family under mcms/<family>/deploy-custom-topology and register it at process startup. Follow this layout:
- register.go — exports Registration() and calls [Registry.Register] in init()
- sequence.go — operations.Sequence that deploys the topology for one chain
- addresses.go — datastore ref resolution / AddressRef construction
- extra_args.go — optional, parses family-specific ChainTopologyConfig.ExtraArgs
The sequence must match Sequence: it receives ChainInput and Deps and returns sequenceutils.OnChainOutput.
Register at startup from init (recommended, mirrors EVM and Solana):
func init() { deploycustomtopology.Registry.Register(Registration()) }
Each family may only be registered once; duplicate registration panics. Use [Registry.RegisteredFamilies] to inspect the registry in tests.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Registry = familyregistry.New[Sequence, ChainInput]("mcms deploy-custom-topology")
Registry holds per-family deploy-custom-topology sequences.
Functions ¶
func EnvFromDeps ¶
func EnvFromDeps(deps Deps) cldf.Environment
EnvFromDeps reconstructs the environment fields sequences need for ref and MCMS resolution.
Types ¶
type ChainInput ¶
type ChainInput struct {
ChainSelector uint64 `json:"chainSelector"`
Config ChainTopologyConfig `json:"config"`
// ExtraArgs mirrors ChainTopologyConfig.ExtraArgs and is parsed by the family
// sequence if needed.
ExtraArgs any `json:"extraArgs,omitempty"`
// MCMS is the proposal input used to build accept-ownership timelock proposals
// when TransferOwnership is set. nil disables proposal building.
MCMS *cldf.MCMSTimelockProposalInput `json:"mcms,omitempty"`
}
ChainInput is the family-agnostic, per-chain request passed to every family sequence by the changeset.
type ChainTopologyConfig ¶
type ChainTopologyConfig struct {
MCMs []MCMSpec `json:"mcms"`
Timelocks []TimelockSpec `json:"timelocks"`
GasBoostConfig *opscontract.GasBoostConfig `json:"gasBoostConfig,omitempty"`
// ExtraArgs carries chain-family-specific input that the default per-chain
// parameters above cannot express. It is forwarded verbatim to the family
// sequence, which parses it if necessary. EVM uses it to carry call-proxy
// settings (see mcms/evm/deploy-custom-topology); Solana ignores it today.
// Future families may require it for family-specific topology input.
ExtraArgs any `json:"extraArgs,omitempty"`
}
ChainTopologyConfig describes the MCMS topology to deploy on a single chain.
type Changeset ¶
type Changeset struct{}
Changeset deploys a custom MCMS topology (arbitrary MCMs + timelocks, custom role wiring, optional ownership transfers) per chain, dispatching to the registered family sequence for each chain. New addresses are written to the datastore; accept-ownership calls are returned as MCMS timelock proposals.
func (Changeset) Apply ¶
func (Changeset) Apply(e cldf.Environment, input Input) (cldf.ChangesetOutput, error)
func (Changeset) VerifyPreconditions ¶
func (Changeset) VerifyPreconditions(env cldf.Environment, input Input) error
type Deps ¶
type Deps struct {
BlockChains chain.BlockChains
DataStore cldfdatastore.DataStore
}
Deps is the read-only dependency bundle available to every family sequence.
type Input ¶
type Input = sequenceutils.WithMCMS[MCMSTopologyConfig]
Input wraps the topology config with optional MCMS timelock-proposal settings. When MCMS is set, ownership-transfer sequences run in no-send mode and the changeset builds accept-ownership timelock proposals from the returned batch operations. Input.MCMS.Qualifier is ignored; each proposal uses the timelock qualifier from the topology.
type MCMSTopologyConfig ¶
type MCMSTopologyConfig struct {
ChainConfigs map[uint64]ChainTopologyConfig `json:"chainConfigs"`
}
MCMSTopologyConfig is the top-level, chain-agnostic changeset input.
type MCMSpec ¶
type MCMSpec struct {
// Ref is a logical handle, unique within the chain. RoleHolder.MCMRef and
// other specs reference an MCM by this Ref.
Ref string `json:"ref"`
Config mcmstypes.Config `json:"config"`
Qualifier string `json:"qualifier"`
ContractType cldf.ContractType `json:"contractType"`
Label *string `json:"label,omitempty"`
}
MCMSpec is one ManyChainMultiSig contract to deploy.
type Registration ¶
type Registration = familyregistry.Registration[Sequence, ChainInput]
Registration describes one chain family's deploy-custom-topology implementation.
type RoleAssignments ¶
type RoleAssignments struct {
Proposers []RoleHolder `json:"proposers"`
Cancellers []RoleHolder `json:"cancellers"`
Bypassers []RoleHolder `json:"bypassers"`
Executors []RoleHolder `json:"executors"` // EVM only
// Admins are optional extra admins; the deployer is always the initial admin
// and the timelock is granted admin over itself (mirrors the legacy flow).
Admins []RoleHolder `json:"admins"` // EVM only - just 1 supported in solana
}
RoleAssignments wires timelock roles to MCMs or addresses.
type RoleHolder ¶
type RoleHolder struct {
MCMRef string `json:"mcmRef,omitempty"`
Address *common.Address `json:"address,omitempty"`
}
RoleHolder identifies a role grantee, either an MCM deployed in this run (by Ref) or a direct address.
type Sequence ¶
type Sequence = operations.Sequence[ChainInput, sequenceutils.OnChainOutput, Deps]
Sequence is the required operations sequence type for all family implementations.
type TimelockSpec ¶
type TimelockSpec struct {
// Ref is a logical handle, unique within the chain and distinct from every
// MCMSpec.Ref on the same chain.
Ref string `json:"ref"`
MinDelay *big.Int `json:"minDelay"`
Qualifier string `json:"qualifier"`
Label *string `json:"label,omitempty"`
Roles RoleAssignments `json:"roles"`
// TransferOwnership (optional): ownable contracts whose ownership should be
// moved to THIS timelock. Duplicate targets are deduplicated by resolved address.
// The deployer runs transferOwnership() now; acceptOwnership() batch ops are
// emitted for MCMS and turned into accept-ownership timelock proposals.
// Requires Input.MCMS to be set.
TransferOwnership []RoleHolder `json:"transferOwnership,omitempty"`
}
TimelockSpec is one timelock (RBACTimelock on EVM) to deploy, with its role wiring and optional ownership transfers.