Skip to content

Commit 54f8ccb

Browse files
ecPabloCopilot
andauthored
chore: port mcms helpers from chainlink/deployment (#923)
We are porting the code in https://github.com/smartcontractkit/chainlink/blob/develop/deployment/common/proposalutils/mcms_helpers.go to CLDF in order to remove deps on core repo for some downstream consumers. The functions were splitted in separate files to keep them under semantically meaningful names. Also added unit tests and fixed some obvious issues pointed out by copilot, but in general keeping the logic as close as possible to what we have in core today. 1. `McmsInspectorForChain` in core repo -> moved to `experimental/proposalutils/inspectors.go` in CLDF. 2. `BatchOperationForChain` and `TransactionForChain` moved to `experimental/proposalutils/operations.go`in CLDF 3. `test_helpers.go` and `types.go` are moved as is from core repo to CLDF Followup PRs in core will attempt to replace usages with these. So we can delete the `mcms_helpers.go` from core ## AI Summary This pull request ports the `proposalutils` helpers from the `chainlink/deployment` repository into the `chainlink-deployments-framework`, making them part of the framework and improving proposal creation, inspection, and testing across multiple blockchain environments. The main changes include adding utility functions for proposal operations, inspectors, and test helpers, as well as defining relevant types and constants for MCMS (Many Chain Multisig) and timelock configurations. **New proposal utilities and helpers:** * Added `experimental/proposalutils/inspectors.go` providing functions to build MCMS inspectors for different chains, supporting optional timelock actions and mass inspector creation. * Added `experimental/proposalutils/operations.go` with helpers to create transactions and batch operations for different chain families (EVM, Solana), abstracting over chain-specific details. **Testing support:** * Added `experimental/proposalutils/test_helpers.go` containing test helpers for signing and executing MCMS proposals and timelock proposals, as well as generating single-group configs and finding call proxy addresses for tests. **Types and constants:** * Added `experimental/proposalutils/types.go` defining types and constants for MCMS roles, contract types, and configuration structures (including gas boost config), supporting both legacy and new MCMS config formats. **Changelog:** * Updated the changeset to document the addition of proposalutils helpers to the framework. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 937e280 commit 54f8ccb

11 files changed

Lines changed: 936 additions & 0 deletions

File tree

.changeset/tame-lamps-lie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
port proposalutils helpers from engine/cld/mcms/proposalutils/ so they are part of the framework
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package proposalutils
2+
3+
import cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
4+
5+
const (
6+
BypasserManyChainMultisig cldf.ContractType = "BypasserManyChainMultiSig"
7+
CancellerManyChainMultisig cldf.ContractType = "CancellerManyChainMultiSig"
8+
ProposerManyChainMultisig cldf.ContractType = "ProposerManyChainMultiSig"
9+
ManyChainMultisig cldf.ContractType = "ManyChainMultiSig"
10+
RBACTimelock cldf.ContractType = "RBACTimelock"
11+
CallProxy cldf.ContractType = "CallProxy"
12+
ManyChainMultisigProgram cldf.ContractType = "ManyChainMultiSigProgram"
13+
RBACTimelockProgram cldf.ContractType = "RBACTimelockProgram"
14+
AccessControllerProgram cldf.ContractType = "AccessControllerProgram"
15+
ProposerAccessControllerAccount cldf.ContractType = "ProposerAccessControllerAccount"
16+
ExecutorAccessControllerAccount cldf.ContractType = "ExecutorAccessControllerAccount"
17+
CancellerAccessControllerAccount cldf.ContractType = "CancellerAccessControllerAccount"
18+
BypasserAccessControllerAccount cldf.ContractType = "BypasserAccessControllerAccount"
19+
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package proposalutils
2+
3+
import (
4+
"fmt"
5+
6+
mcmschainwrappers "github.com/smartcontractkit/mcms/chainwrappers"
7+
mcmssdk "github.com/smartcontractkit/mcms/sdk"
8+
9+
mcmstypes "github.com/smartcontractkit/mcms/types"
10+
11+
cldfmcmsadapters "github.com/smartcontractkit/chainlink-deployments-framework/chain/mcms/adapters"
12+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
13+
)
14+
15+
type mcmsInspectorOptions struct {
16+
TimelockAction mcmstypes.TimelockAction
17+
}
18+
19+
// MCMSInspectorOption configures how MCMS inspectors are built.
20+
type MCMSInspectorOption func(*mcmsInspectorOptions)
21+
22+
// WithTimelockAction sets the timelock action used by the inspector.
23+
// When omitted, the default action is TimelockActionSchedule.
24+
func WithTimelockAction(action mcmstypes.TimelockAction) MCMSInspectorOption {
25+
return func(opts *mcmsInspectorOptions) {
26+
opts.TimelockAction = action
27+
}
28+
}
29+
30+
// McmsInspectorForChain builds an mcmssdk.Inspector for a single chain in the given environment.
31+
// The chain must be present in env.BlockChains, otherwise an error is returned.
32+
func McmsInspectorForChain(env cldf.Environment, chain uint64, opts ...MCMSInspectorOption) (mcmssdk.Inspector, error) {
33+
var options mcmsInspectorOptions
34+
for _, opt := range opts {
35+
opt(&options)
36+
}
37+
38+
action := mcmstypes.TimelockActionSchedule
39+
if options.TimelockAction != "" {
40+
action = options.TimelockAction
41+
}
42+
43+
chainAccessor := cldfmcmsadapters.Wrap(env.BlockChains)
44+
45+
return mcmschainwrappers.BuildInspector(&chainAccessor, mcmstypes.ChainSelector(chain), action,
46+
mcmstypes.ChainMetadata{})
47+
}
48+
49+
// McmsInspectors builds an mcmssdk.Inspector for every chain in the environment,
50+
// returning them keyed by uint64 chain selector. All inspectors use the default
51+
// TimelockActionSchedule action.
52+
func McmsInspectors(env cldf.Environment) (map[uint64]mcmssdk.Inspector, error) {
53+
chainsMetadata := map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata{}
54+
for chainSelector := range env.BlockChains.All() {
55+
chainsMetadata[mcmstypes.ChainSelector(chainSelector)] = mcmstypes.ChainMetadata{}
56+
}
57+
58+
chainAccessor := cldfmcmsadapters.Wrap(env.BlockChains)
59+
60+
mcmsInspectors, err := mcmschainwrappers.BuildInspectors(&chainAccessor, chainsMetadata, mcmstypes.TimelockActionSchedule)
61+
if err != nil {
62+
return nil, fmt.Errorf("failed to build inspectors: %w", err)
63+
}
64+
65+
inspectors := make(map[uint64]mcmssdk.Inspector, len(mcmsInspectors))
66+
for chainSelector, inspector := range mcmsInspectors {
67+
inspectors[uint64(chainSelector)] = inspector
68+
}
69+
70+
return inspectors, nil
71+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package proposalutils
2+
3+
import (
4+
"testing"
5+
6+
chainsel "github.com/smartcontractkit/chain-selectors"
7+
mcmstypes "github.com/smartcontractkit/mcms/types"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/smartcontractkit/chainlink-deployments-framework/chain"
12+
cldfevm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
13+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
14+
)
15+
16+
func TestWithTimelockAction(t *testing.T) {
17+
t.Parallel()
18+
19+
tests := []struct {
20+
name string
21+
action mcmstypes.TimelockAction
22+
}{
23+
{
24+
name: "sets schedule action",
25+
action: mcmstypes.TimelockActionSchedule,
26+
},
27+
{
28+
name: "sets cancel action",
29+
action: mcmstypes.TimelockActionCancel,
30+
},
31+
{
32+
name: "sets bypass action",
33+
action: mcmstypes.TimelockActionBypass,
34+
},
35+
}
36+
37+
for _, tt := range tests {
38+
t.Run(tt.name, func(t *testing.T) {
39+
t.Parallel()
40+
41+
var opts mcmsInspectorOptions
42+
WithTimelockAction(tt.action)(&opts)
43+
assert.Equal(t, tt.action, opts.TimelockAction)
44+
})
45+
}
46+
}
47+
48+
func TestMcmsInspectorForChain(t *testing.T) {
49+
t.Parallel()
50+
51+
evmSelector := chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector
52+
53+
tests := []struct {
54+
name string
55+
chains map[uint64]chain.BlockChain
56+
chain uint64
57+
opts []MCMSInspectorOption
58+
wantErr string
59+
}{
60+
{
61+
name: "success with default action",
62+
chains: map[uint64]chain.BlockChain{
63+
evmSelector: cldfevm.Chain{Selector: evmSelector},
64+
},
65+
chain: evmSelector,
66+
},
67+
{
68+
name: "success with custom action",
69+
chains: map[uint64]chain.BlockChain{
70+
evmSelector: cldfevm.Chain{Selector: evmSelector},
71+
},
72+
chain: evmSelector,
73+
opts: []MCMSInspectorOption{WithTimelockAction(mcmstypes.TimelockActionBypass)},
74+
},
75+
{
76+
name: "error when chain not in environment",
77+
chains: nil,
78+
chain: evmSelector,
79+
wantErr: "missing",
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
t.Parallel()
86+
87+
env := cldf.Environment{
88+
BlockChains: chain.NewBlockChains(tt.chains),
89+
}
90+
91+
inspector, err := McmsInspectorForChain(env, tt.chain, tt.opts...)
92+
93+
if tt.wantErr != "" {
94+
require.ErrorContains(t, err, tt.wantErr)
95+
assert.Nil(t, inspector)
96+
97+
return
98+
}
99+
100+
require.NoError(t, err)
101+
assert.NotNil(t, inspector)
102+
})
103+
}
104+
}
105+
106+
func TestMcmsInspectors(t *testing.T) {
107+
t.Parallel()
108+
109+
evmSelector1 := chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector
110+
evmSelector2 := chainsel.ETHEREUM_MAINNET.Selector
111+
112+
tests := []struct {
113+
name string
114+
chains map[uint64]chain.BlockChain
115+
wantLen int
116+
wantSelectors []uint64
117+
}{
118+
{
119+
name: "empty blockchains returns empty map",
120+
chains: nil,
121+
wantLen: 0,
122+
wantSelectors: nil,
123+
},
124+
{
125+
name: "single chain returns single inspector with uint64 key",
126+
chains: map[uint64]chain.BlockChain{
127+
evmSelector1: cldfevm.Chain{Selector: evmSelector1},
128+
},
129+
wantLen: 1,
130+
wantSelectors: []uint64{evmSelector1},
131+
},
132+
{
133+
name: "multiple chains returns inspector per chain",
134+
chains: map[uint64]chain.BlockChain{
135+
evmSelector1: cldfevm.Chain{Selector: evmSelector1},
136+
evmSelector2: cldfevm.Chain{Selector: evmSelector2},
137+
},
138+
wantLen: 2,
139+
wantSelectors: []uint64{evmSelector1, evmSelector2},
140+
},
141+
}
142+
143+
for _, tt := range tests {
144+
t.Run(tt.name, func(t *testing.T) {
145+
t.Parallel()
146+
147+
env := cldf.Environment{
148+
BlockChains: chain.NewBlockChains(tt.chains),
149+
}
150+
151+
inspectors, err := McmsInspectors(env)
152+
require.NoError(t, err)
153+
require.Len(t, inspectors, tt.wantLen)
154+
155+
for _, sel := range tt.wantSelectors {
156+
assert.NotNil(t, inspectors[sel], "expected inspector for selector %d", sel)
157+
}
158+
})
159+
}
160+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package proposalutils
2+
3+
// MCMSRole represents a named role within the MCMS system (e.g. proposer, bypasser, canceller).
4+
type MCMSRole string
5+
6+
const (
7+
ProposerRole MCMSRole = "PROPOSER"
8+
BypasserRole MCMSRole = "BYPASSER"
9+
CancellerRole MCMSRole = "CANCELLER"
10+
)
11+
12+
func (role MCMSRole) String() string {
13+
return string(role)
14+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package proposalutils
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestMCMSRole_String(t *testing.T) {
10+
t.Parallel()
11+
12+
tests := []struct {
13+
name string
14+
role MCMSRole
15+
want string
16+
}{
17+
{name: "proposer", role: ProposerRole, want: "PROPOSER"},
18+
{name: "bypasser", role: BypasserRole, want: "BYPASSER"},
19+
{name: "canceller", role: CancellerRole, want: "CANCELLER"},
20+
{name: "custom role", role: MCMSRole("CUSTOM"), want: "CUSTOM"},
21+
{name: "empty", role: MCMSRole(""), want: ""},
22+
}
23+
24+
for _, tt := range tests {
25+
t.Run(tt.name, func(t *testing.T) {
26+
t.Parallel()
27+
assert.Equal(t, tt.want, tt.role.String())
28+
})
29+
}
30+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package proposalutils
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/gagliardetto/solana-go"
9+
chain_selectors "github.com/smartcontractkit/chain-selectors"
10+
mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm"
11+
mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana"
12+
mcmstypes "github.com/smartcontractkit/mcms/types"
13+
)
14+
15+
// TransactionForChain builds an mcmstypes.Transaction for the given chain selector.
16+
// It currently supports EVM and Solana chains; other chain families return an error.
17+
func TransactionForChain(
18+
chain uint64, toAddress string, data []byte, value *big.Int, contractType string, tags []string,
19+
) (mcmstypes.Transaction, error) {
20+
chainFamily, err := mcmstypes.GetChainSelectorFamily(mcmstypes.ChainSelector(chain))
21+
if err != nil {
22+
return mcmstypes.Transaction{}, fmt.Errorf("failed to get chain family for chain %d: %w", chain, err)
23+
}
24+
25+
var tx mcmstypes.Transaction
26+
27+
switch chainFamily {
28+
case chain_selectors.FamilyEVM:
29+
if !common.IsHexAddress(toAddress) {
30+
return mcmstypes.Transaction{}, fmt.Errorf("invalid EVM address: %s", toAddress)
31+
}
32+
tx = mcmsevmsdk.NewTransaction(common.HexToAddress(toAddress), data, value, contractType, tags)
33+
34+
case chain_selectors.FamilySolana:
35+
accounts := []*solana.AccountMeta{} // FIXME: how to pass accounts to support solana?
36+
var err error
37+
tx, err = mcmssolanasdk.NewTransaction(toAddress, data, value, accounts, contractType, tags)
38+
if err != nil {
39+
return mcmstypes.Transaction{}, fmt.Errorf("failed to create solana transaction: %w", err)
40+
}
41+
42+
default:
43+
return mcmstypes.Transaction{}, fmt.Errorf("unsupported chain family %s", chainFamily)
44+
}
45+
46+
return tx, nil
47+
}
48+
49+
// BatchOperationForChain creates an mcmstypes.BatchOperation containing a single transaction
50+
// for the given chain selector. It delegates to TransactionForChain, so it supports EVM and
51+
// Solana chains.
52+
func BatchOperationForChain(
53+
chain uint64, toAddress string, data []byte, value *big.Int, contractType string, tags []string,
54+
) (mcmstypes.BatchOperation, error) {
55+
tx, err := TransactionForChain(chain, toAddress, data, value, contractType, tags)
56+
if err != nil {
57+
return mcmstypes.BatchOperation{}, fmt.Errorf("failed to create transaction for chain: %w", err)
58+
}
59+
60+
return mcmstypes.BatchOperation{
61+
ChainSelector: mcmstypes.ChainSelector(chain),
62+
Transactions: []mcmstypes.Transaction{tx},
63+
}, nil
64+
}

0 commit comments

Comments
 (0)