Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
github.com/spf13/cast v1.10.0
github.com/stretchr/testify v1.11.1
github.com/zksync-sdk/zksync2-go v1.1.1-0.20250620124214-2c742ee399c6
golang.org/x/crypto v0.51.0
golang.org/x/mod v0.36.0
golang.org/x/sync v0.20.0
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -306,7 +307,6 @@ require (
go.uber.org/zap v1.28.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
Expand Down
61 changes: 61 additions & 0 deletions internal/mcmsrole/role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Package mcmsrole provides RBACTimelock role names and IDs used by MCMS changesets.
//
// RBACTimelock defines five roles: ADMIN, PROPOSER, EXECUTOR, BYPASSER, and
// CANCELLER. Each role ID is the keccak256 hash of its name string, matching
// OpenZeppelin AccessControl. See RBACTimelock in ccip-owner-contracts:
// https://github.com/smartcontractkit/ccip-owner-contracts/blob/9d81692b324ce7ea2ef8a75e683889edbc7e2dd0/src/RBACTimelock.sol#L71
//
// These constants are defined here instead of via generated Go bindings so
// changesets can reference role IDs without importing timelock contract wrappers.
//
// The package lives under internal/ because both legacy and top-level changesets
// depend on it. Once MCMS changesets are consolidated, consider moving it into
// the mcms changesets tree or promoting it to the mcms library.
package mcmsrole

import (
"github.com/ethereum/go-ethereum/common"
"golang.org/x/crypto/sha3"
)

// Each role string maps to the role names in the RBACTimelock contract.
// https://github.com/smartcontractkit/ccip-owner-contracts/blob/9d81692b324ce7ea2ef8a75e683889edbc7e2dd0/src/RBACTimelock.sol#L71-L75
const (
adminRoleStr = "ADMIN_ROLE"
proposerRoleStr = "PROPOSER_ROLE"
bypasserRoleStr = "BYPASSER_ROLE" //nolint:gosec // G101: These are not secrets and only used in tests.
cancellerRoleStr = "CANCELLER_ROLE"
executorRoleStr = "EXECUTOR_ROLE"
)

var (
AdminRole = NewRole(adminRoleStr)
ProposerRole = NewRole(proposerRoleStr)
BypasserRole = NewRole(bypasserRoleStr)
CancellerRole = NewRole(cancellerRoleStr)
ExecutorRole = NewRole(executorRoleStr)
)

// Role represents a role in the MCMS Timelock contracts.
type Role struct {
ID common.Hash
Name string
}

// NewRole creates a new role with the given name and calculates the ID.
func NewRole(name string) Role {
return Role{
ID: mustKeccakHash(name),
Name: name,
}
}

// mustKeccakHash calculates the keccak256 hash of the input string.
func mustKeccakHash(in string) common.Hash {
hash := sha3.NewLegacyKeccak256()
if _, err := hash.Write([]byte(in)); err != nil {
panic(err)
}

return common.BytesToHash(hash.Sum(nil))
}
77 changes: 77 additions & 0 deletions internal/mcmsrole/role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package mcmsrole

import (
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)

func TestNewRole(t *testing.T) {
t.Parallel()

tests := []struct {
name string
roleName string
wantID common.Hash
}{
{
name: "admin",
roleName: "ADMIN_ROLE",
wantID: common.HexToHash("0xa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775"),
},
{
name: "proposer",
roleName: "PROPOSER_ROLE",
wantID: common.HexToHash("0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1"),
},
{
name: "executor",
roleName: "EXECUTOR_ROLE",
wantID: common.HexToHash("0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63"),
},
{
name: "bypasser",
roleName: "BYPASSER_ROLE",
wantID: common.HexToHash("0xa1b2b8005de234c4b8ce8cd0be058239056e0d54f6097825b5117101469d5a8d"),
},
{
name: "canceller",
roleName: "CANCELLER_ROLE",
wantID: common.HexToHash("0xfd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f783"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

got := NewRole(tt.roleName)
require.Equal(t, tt.roleName, got.Name)
require.Equal(t, tt.wantID, got.ID)
})
}
}

func TestPredefinedRoles(t *testing.T) {
t.Parallel()

tests := []struct {
name string
role Role
}{
{name: "admin", role: AdminRole},
{name: "proposer", role: ProposerRole},
{name: "executor", role: ExecutorRole},
{name: "bypasser", role: BypasserRole},
{name: "canceller", role: CancellerRole},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

require.Equal(t, NewRole(tt.role.Name), tt.role)
})
}
}
9 changes: 1 addition & 8 deletions legacy/pkg/family/evm/changesets/deploy_mcms_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package changesets

import (
"encoding/json"
"testing"

chainsel "github.com/smartcontractkit/chain-selectors"
Expand Down Expand Up @@ -84,12 +83,6 @@ func TestDeployMCMSWithTimelockContracts(t *testing.T) {
require.NoError(t, err)
require.Len(t, addresses, 5)

mcmsState, err := evmstate.MaybeLoadMCMSWithTimelockChainState(chain, addresses)
require.NoError(t, err)

v, err := mcmsState.GenerateMCMSWithTimelockView()
require.NoError(t, err)

_, err = json.MarshalIndent(v, "", " ")
_, err = evmstate.MaybeLoadMCMSWithTimelockChainState(chain, addresses)
require.NoError(t, err)
}
26 changes: 13 additions & 13 deletions legacy/pkg/family/evm/changesets/deploy_mcms_with_timelock.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import (
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
"github.com/spf13/cast"

"github.com/smartcontractkit/cld-changesets/internal/mcmsrole"
evmstate "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/evm"
"github.com/smartcontractkit/cld-changesets/pkg/contract/mcms/view/v1_0"
opsevm "github.com/smartcontractkit/cld-changesets/pkg/family/evm/operations"
seqs "github.com/smartcontractkit/cld-changesets/pkg/family/evm/sequences"
)
Expand Down Expand Up @@ -300,7 +300,7 @@ func DeployMCMSWithTimelockContractsEVM(
func getAdminAddresses(ctx context.Context, timelock *bindings.RBACTimelock) ([]string, error) {
numAddresses, err := timelock.GetRoleMemberCount(&bind.CallOpts{
Context: ctx,
}, v1_0.ADMIN_ROLE.ID)
}, mcmsrole.AdminRole.ID)
if err != nil {
return nil, err
}
Expand All @@ -315,7 +315,7 @@ func getAdminAddresses(ctx context.Context, timelock *bindings.RBACTimelock) ([]
}
address, err := timelock.GetRoleMember(&bind.CallOpts{
Context: ctx,
}, v1_0.ADMIN_ROLE.ID, big.NewInt(idx))
}, mcmsrole.AdminRole.ID, big.NewInt(idx))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -372,23 +372,23 @@ func GrantRolesForTimelock(
IsDeployerKeyAdmin: isDeployerKeyAdmin,
RolesAndAddresses: []seqs.RolesAndAddresses{
{
Role: v1_0.PROPOSER_ROLE.ID,
Name: v1_0.PROPOSER_ROLE.Name,
Role: mcmsrole.ProposerRole.ID,
Name: mcmsrole.ProposerRole.Name,
Addresses: []common.Address{proposer.Address()},
},
{
Role: v1_0.CANCELLER_ROLE.ID,
Name: v1_0.CANCELLER_ROLE.Name,
Role: mcmsrole.CancellerRole.ID,
Name: mcmsrole.CancellerRole.Name,
Addresses: []common.Address{proposer.Address(), canceller.Address(), bypasser.Address()},
},
{
Role: v1_0.BYPASSER_ROLE.ID,
Name: v1_0.BYPASSER_ROLE.Name,
Role: mcmsrole.BypasserRole.ID,
Name: mcmsrole.BypasserRole.Name,
Addresses: []common.Address{bypasser.Address()},
},
{
Role: v1_0.EXECUTOR_ROLE.ID,
Name: v1_0.EXECUTOR_ROLE.Name,
Role: mcmsrole.ExecutorRole.ID,
Name: mcmsrole.ExecutorRole.Name,
Addresses: []common.Address{callProxy.Address()},
},
},
Expand All @@ -398,8 +398,8 @@ func GrantRolesForTimelock(
if !isTimelockAdmin {
// We grant the timelock the admin role on the MCMS contracts.
seqInput.RolesAndAddresses = append(seqInput.RolesAndAddresses, seqs.RolesAndAddresses{
Role: v1_0.ADMIN_ROLE.ID,
Name: v1_0.ADMIN_ROLE.Name,
Role: mcmsrole.AdminRole.ID,
Name: mcmsrole.AdminRole.Name,
Addresses: []common.Address{timelock.Address()},
})
}
Expand Down
10 changes: 0 additions & 10 deletions legacy/pkg/family/evm/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms"

"github.com/smartcontractkit/cld-changesets/internal/semvers"
"github.com/smartcontractkit/cld-changesets/pkg/contract/mcms/view/v1_0"
)

// MCMSWithTimelockState holds the Go bindings
Expand Down Expand Up @@ -50,15 +49,6 @@ func (state MCMSWithTimelockState) Validate() error {
return nil
}

func (state MCMSWithTimelockState) GenerateMCMSWithTimelockView() (v1_0.MCMSWithTimelockView, error) {
if err := state.Validate(); err != nil {
return v1_0.MCMSWithTimelockView{}, fmt.Errorf("unable to validate McmsWithTimelock state: %w", err)
}

return v1_0.GenerateMCMSWithTimelockView(*state.BypasserMcm, *state.CancellerMcm, *state.ProposerMcm,
*state.Timelock, *state.CallProxy)
}

// MaybeLoadMCMSWithTimelockState loads the MCMSWithTimelockState state for each chain in the given environment.
func MaybeLoadMCMSWithTimelockState(env cldf.Environment, chainSelectors []uint64) (map[uint64]*MCMSWithTimelockState, error) {
return MaybeLoadMCMSWithTimelockStateWithQualifier(env, chainSelectors, "")
Expand Down
Loading
Loading