Skip to content

Commit 1726d0d

Browse files
authored
Merge branch 'main' into fix-select-fee-quoter-ABI-by-fee-quoter-version
2 parents ee5188d + 2db1389 commit 1726d0d

34 files changed

Lines changed: 1377 additions & 93 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,5 @@ devenv/env-out.toml
6262
.cursorrules
6363
.claude/
6464
operations-gen
65+
.agents/
66+
AGENTS.md
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package hooks
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
8+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
9+
"github.com/ethereum/go-ethereum/common"
10+
chain_selectors "github.com/smartcontractkit/chain-selectors"
11+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
12+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/provider/rpcclient"
13+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
14+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config"
15+
cfgnet "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/network"
16+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain"
17+
18+
evm_datastore_utils "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/datastore"
19+
mcms_ops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations"
20+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/rmn_proxy"
21+
mcms_seq "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/sequences"
22+
routerops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router"
23+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/token_admin_registry"
24+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/rmn_remote"
25+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/committee_verifier"
26+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/executor"
27+
fqops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/fee_quoter"
28+
offrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/offramp"
29+
onrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/onramp"
30+
seq2_0 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/sequences"
31+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/versioned_verifier_resolver"
32+
cciphooks "github.com/smartcontractkit/chainlink-ccip/deployment/hooks"
33+
common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils"
34+
datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore"
35+
)
36+
37+
var _ cciphooks.ContractOwnership = (*EVMContractOwnership)(nil)
38+
39+
func init() {
40+
cciphooks.GetContractOwnershipRegistry().Register(chain_selectors.FamilyEVM, &EVMContractOwnership{})
41+
}
42+
43+
var contractTypesForOwnershipCheck = map[datastore.ContractType]struct{}{
44+
datastore.ContractType(committee_verifier.ContractType): {},
45+
datastore.ContractType(executor.ContractType): {},
46+
datastore.ContractType(seq2_0.ExecutorProxyType): {},
47+
datastore.ContractType(onrampops.ContractType): {},
48+
datastore.ContractType(offrampops.ContractType): {},
49+
datastore.ContractType(fqops.ContractType): {},
50+
datastore.ContractType(routerops.ContractType): {},
51+
datastore.ContractType(rmn_remote.ContractType): {},
52+
datastore.ContractType(rmn_proxy.ContractType): {},
53+
datastore.ContractType(token_admin_registry.ContractType): {},
54+
datastore.ContractType(common_utils.BypasserManyChainMultisig): {},
55+
datastore.ContractType(common_utils.CancellerManyChainMultisig): {},
56+
datastore.ContractType(common_utils.ProposerManyChainMultisig): {},
57+
datastore.ContractType(versioned_verifier_resolver.CCTPVerifierResolverType): {},
58+
datastore.ContractType(versioned_verifier_resolver.CommitteeVerifierResolverType): {},
59+
datastore.ContractType(versioned_verifier_resolver.LombardVerifierResolverType): {},
60+
datastore.ContractType(versioned_verifier_resolver.CommitteeVerifierContractType): {},
61+
datastore.ContractType(versioned_verifier_resolver.ContractType): {},
62+
}
63+
64+
// EVMContractOwnership validates that contracts are owned by expected timelocks.
65+
type EVMContractOwnership struct {
66+
cllccipTimelockAddr sync.Map // map[chainSelector]timelockAddress for CLLCCIP RBACTimelock
67+
rmntimelockAddr sync.Map // map[chainSelector]timelockAddress for RMNMCMS RBACTimelock
68+
}
69+
70+
func (e *EVMContractOwnership) timelocksInOwnershipCheck(ds datastore.DataStore, chainSelector uint64) error {
71+
cllTL, clltlExists := e.loadTimelockFromCache(&e.cllccipTimelockAddr, chainSelector)
72+
rmnTL, rmntlExists := e.loadTimelockFromCache(&e.rmntimelockAddr, chainSelector)
73+
if clltlExists && rmntlExists && cllTL != (common.Address{}) && rmnTL != (common.Address{}) {
74+
return nil
75+
}
76+
cllccipTimelockAddr, err := datastore_utils.FindAndFormatRef(ds, datastore.AddressRef{
77+
Type: datastore.ContractType(common_utils.RBACTimelock),
78+
Qualifier: common_utils.CLLQualifier,
79+
}, chainSelector, evm_datastore_utils.ToEVMAddress)
80+
if err != nil {
81+
return fmt.Errorf("ownership transfer requires CLLCCIP RBACTimelock in ExistingAddresses: %w", err)
82+
}
83+
84+
rmnTimelockAddr, err := datastore_utils.FindAndFormatRef(ds, datastore.AddressRef{
85+
Type: datastore.ContractType(common_utils.RBACTimelock),
86+
Qualifier: common_utils.RMNTimelockQualifier,
87+
}, chainSelector, evm_datastore_utils.ToEVMAddress)
88+
if err != nil {
89+
return fmt.Errorf("ownership transfer requires RMNMCMS RBACTimelock in ExistingAddresses: %w", err)
90+
}
91+
e.cllccipTimelockAddr.Store(chainSelector, cllccipTimelockAddr)
92+
e.rmntimelockAddr.Store(chainSelector, rmnTimelockAddr)
93+
return nil
94+
}
95+
96+
func (e *EVMContractOwnership) loadTimelockFromCache(cache *sync.Map, chainSelector uint64) (common.Address, bool) {
97+
value, ok := cache.Load(chainSelector)
98+
if !ok {
99+
return common.Address{}, false
100+
}
101+
addr, isAddress := value.(common.Address)
102+
if !isAddress {
103+
return common.Address{}, false
104+
}
105+
return addr, true
106+
}
107+
108+
func (e *EVMContractOwnership) FilterNetworks(envName string, dom domain.Domain, lggr logger.Logger) (*cfgnet.Config, error) {
109+
networkCfg, err := config.LoadNetworks(envName, dom, lggr)
110+
if err != nil {
111+
return nil, err
112+
}
113+
return networkCfg.FilterWith(cfgnet.ChainFamilyFilter(chain_selectors.FamilyEVM)), nil
114+
}
115+
116+
func (e *EVMContractOwnership) NeedsOwnershipCheck(ref datastore.AddressRef) bool {
117+
_, exists := contractTypesForOwnershipCheck[ref.Type]
118+
return exists
119+
}
120+
121+
func (e *EVMContractOwnership) expectedOwnerForRef(ref datastore.AddressRef) (common.Address, error) {
122+
switch ref.Type {
123+
case datastore.ContractType(rmn_remote.ContractType):
124+
addr, ok := e.loadTimelockFromCache(&e.rmntimelockAddr, ref.ChainSelector)
125+
if !ok {
126+
return common.Address{}, fmt.Errorf("RMNMCMS RBACTimelock address not found for chain selector %d", ref.ChainSelector)
127+
}
128+
return addr, nil
129+
default:
130+
addr, ok := e.loadTimelockFromCache(&e.cllccipTimelockAddr, ref.ChainSelector)
131+
if !ok {
132+
return common.Address{}, fmt.Errorf("CLLCCIP RBACTimelock address not found for chain selector %d", ref.ChainSelector)
133+
}
134+
return addr, nil
135+
}
136+
}
137+
138+
func (e *EVMContractOwnership) VerifyContractOwnership(
139+
ctx context.Context,
140+
lggr logger.Logger,
141+
ds datastore.DataStore,
142+
network cfgnet.Network,
143+
refsToCheck []datastore.AddressRef,
144+
) error {
145+
if len(network.RPCs) == 0 || network.RPCs[0].HTTPURL == "" {
146+
return fmt.Errorf("network %d has no HTTP RPC configured", network.ChainSelector)
147+
}
148+
// TODO use blockchains from Env when chains are included in hookEnv
149+
rpcCfg := rpcclient.RPCConfig{
150+
ChainSelector: network.ChainSelector,
151+
}
152+
for _, rpc := range network.RPCs {
153+
p, err := rpcclient.URLSchemePreferenceFromString(rpc.PreferredURLScheme)
154+
if err != nil {
155+
return fmt.Errorf("invalid preferred URL scheme for RPC %s on network %d: %w", rpc.RPCName, network.ChainSelector, err)
156+
}
157+
rpcCfg.RPCs = append(rpcCfg.RPCs, rpcclient.RPC{
158+
Name: rpc.RPCName,
159+
WSURL: rpc.WSURL,
160+
HTTPURL: rpc.HTTPURL,
161+
PreferredURLScheme: p,
162+
})
163+
}
164+
client, err := rpcclient.NewMultiClient(ctx, lggr, rpcCfg)
165+
if err != nil {
166+
return fmt.Errorf("dial RPC for chain %d: %w", network.ChainSelector, err)
167+
}
168+
defer client.Close()
169+
if err := e.timelocksInOwnershipCheck(ds, network.ChainSelector); err != nil {
170+
return fmt.Errorf("initialize timelocks for chain %d: %w", network.ChainSelector, err)
171+
}
172+
var acceptableAdmins []common.Address
173+
cllTL, ok := e.cllccipTimelockAddr.Load(network.ChainSelector)
174+
if ok {
175+
acceptableAdmins = append(acceptableAdmins, cllTL.(common.Address))
176+
}
177+
rmnTL, ok := e.rmntimelockAddr.Load(network.ChainSelector)
178+
if ok {
179+
acceptableAdmins = append(acceptableAdmins, rmnTL.(common.Address))
180+
}
181+
for _, ref := range refsToCheck {
182+
if ref.Type == datastore.ContractType(common_utils.RBACTimelock) {
183+
// only checking if timelock is governed by timelock
184+
// TODO: check if deployer key is not admin when the blockchains are available in HookEnv
185+
timelock, err := mcms_seq.LoadTimelockContract(common.HexToAddress(ref.Address), client)
186+
if err != nil {
187+
return fmt.Errorf("failed to load timelock contract %s: %w", ref.Address, err)
188+
}
189+
adminFound := false
190+
for _, addr := range acceptableAdmins {
191+
adminHasRole, err := timelock.HasRole(&bind.CallOpts{
192+
Context: ctx,
193+
}, mcms_ops.ADMIN_ROLE.ID, addr)
194+
if err != nil {
195+
return fmt.Errorf("failed to check admin role for acceptable admin %s on timelock %s: %w", addr.Hex(), ref.Address, err)
196+
}
197+
if adminHasRole {
198+
adminFound = true
199+
break
200+
}
201+
}
202+
if !adminFound {
203+
return fmt.Errorf("ownership check failed for timelock %s: none of the acceptable admins %v have admin role", ref.Address, acceptableAdmins)
204+
}
205+
}
206+
addr, err := evm_datastore_utils.ToEVMAddress(ref)
207+
if err != nil {
208+
return fmt.Errorf("error formatting address ref %s for contract type %s version %s on chain %d: %w",
209+
ref.Address, ref.Type, ref.Version, network.ChainSelector, err)
210+
}
211+
currentOwner, _, err := mcms_seq.LoadOwnableContract(addr, client)
212+
if err != nil {
213+
return fmt.Errorf("failed to load ownable contract %s (%s): %w", addr, ref.Type, err)
214+
}
215+
expectedOwner, err := e.expectedOwnerForRef(ref)
216+
if err != nil {
217+
return fmt.Errorf("failed to determine expected owner for contract %s (%s): %w", addr, ref.Type, err)
218+
}
219+
if currentOwner != expectedOwner {
220+
return fmt.Errorf("ownership check failed for contract %s (%s): expected owner %s, got %s",
221+
addr, ref.Type, expectedOwner, currentOwner)
222+
}
223+
lggr.Infof("ownership check passed for contract %s (%s): owner is %s", addr, ref.Type, currentOwner)
224+
}
225+
return nil
226+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package hooks
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
chainsel "github.com/smartcontractkit/chain-selectors"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
11+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
12+
cfgnet "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/network"
13+
14+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/rmn_proxy"
15+
routerops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router"
16+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/token_admin_registry"
17+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/rmn_remote"
18+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/committee_verifier"
19+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/executor"
20+
fqops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/fee_quoter"
21+
offrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/offramp"
22+
onrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/onramp"
23+
seq2_0 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/sequences"
24+
common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils"
25+
)
26+
27+
func TestTimelocksInOwnershipCheck_LoadsAndCaches(t *testing.T) {
28+
selector := chainsel.ETHEREUM_MAINNET.Selector
29+
ds := datastore.NewMemoryDataStore()
30+
require.NoError(t, ds.Addresses().Add(datastore.AddressRef{
31+
ChainSelector: selector,
32+
Type: datastore.ContractType(common_utils.RBACTimelock),
33+
Qualifier: common_utils.CLLQualifier,
34+
Address: "0x00000000000000000000000000000000000000A1",
35+
}))
36+
require.NoError(t, ds.Addresses().Add(datastore.AddressRef{
37+
ChainSelector: selector,
38+
Type: datastore.ContractType(common_utils.RBACTimelock),
39+
Qualifier: common_utils.RMNTimelockQualifier,
40+
Address: "0x00000000000000000000000000000000000000B1",
41+
}))
42+
43+
e := &EVMContractOwnership{}
44+
require.NoError(t, e.timelocksInOwnershipCheck(ds.Seal(), selector))
45+
46+
cll, ok := e.cllccipTimelockAddr.Load(selector)
47+
require.True(t, ok)
48+
require.Equal(t, common.HexToAddress("0x00000000000000000000000000000000000000A1"), cll.(common.Address))
49+
rmn, ok := e.rmntimelockAddr.Load(selector)
50+
require.True(t, ok)
51+
require.Equal(t, common.HexToAddress("0x00000000000000000000000000000000000000B1"), rmn.(common.Address))
52+
53+
// cache hit should not need datastore lookups again.
54+
require.NoError(t, e.timelocksInOwnershipCheck(datastore.NewMemoryDataStore().Seal(), selector))
55+
}
56+
57+
func TestTimelocksInOwnershipCheck_MissingCLLTimelock(t *testing.T) {
58+
selector := chainsel.ETHEREUM_MAINNET.Selector
59+
ds := datastore.NewMemoryDataStore()
60+
e := &EVMContractOwnership{}
61+
err := e.timelocksInOwnershipCheck(ds.Seal(), selector)
62+
require.Error(t, err)
63+
require.ErrorContains(t, err, "ownership transfer requires CLLCCIP RBACTimelock")
64+
}
65+
66+
func TestExpectedOwnerForRef_UsesRMNTimelockForRMNRemote(t *testing.T) {
67+
selector := chainsel.ETHEREUM_MAINNET.Selector
68+
e := &EVMContractOwnership{}
69+
e.cllccipTimelockAddr.Store(selector, common.HexToAddress("0x00000000000000000000000000000000000000A1"))
70+
e.rmntimelockAddr.Store(selector, common.HexToAddress("0x00000000000000000000000000000000000000B1"))
71+
72+
normal, err := e.expectedOwnerForRef(datastore.AddressRef{
73+
ChainSelector: selector,
74+
Type: "AnyType",
75+
})
76+
require.NoError(t, err)
77+
rmn, err := e.expectedOwnerForRef(datastore.AddressRef{
78+
ChainSelector: selector,
79+
Type: datastore.ContractType(rmn_remote.ContractType),
80+
})
81+
require.NoError(t, err)
82+
require.Equal(t, common.HexToAddress("0x00000000000000000000000000000000000000A1"), normal)
83+
require.Equal(t, common.HexToAddress("0x00000000000000000000000000000000000000B1"), rmn)
84+
}
85+
86+
func TestExpectedOwnerForRef_MissingTimelockReturnsError(t *testing.T) {
87+
selector := chainsel.ETHEREUM_MAINNET.Selector
88+
e := &EVMContractOwnership{}
89+
90+
_, err := e.expectedOwnerForRef(datastore.AddressRef{
91+
ChainSelector: selector,
92+
Type: "AnyType",
93+
})
94+
require.Error(t, err)
95+
require.ErrorContains(t, err, "CLLCCIP RBACTimelock address not found")
96+
97+
_, err = e.expectedOwnerForRef(datastore.AddressRef{
98+
ChainSelector: selector,
99+
Type: datastore.ContractType(rmn_remote.ContractType),
100+
})
101+
require.Error(t, err)
102+
require.ErrorContains(t, err, "RMNMCMS RBACTimelock address not found")
103+
}
104+
105+
func TestNeedsOwnershipCheck_UsesLaneMigratorContractTypes(t *testing.T) {
106+
e := &EVMContractOwnership{}
107+
allowedTypes := []datastore.ContractType{
108+
datastore.ContractType(committee_verifier.ContractType),
109+
datastore.ContractType(executor.ContractType),
110+
datastore.ContractType(seq2_0.ExecutorProxyType),
111+
datastore.ContractType(onrampops.ContractType),
112+
datastore.ContractType(offrampops.ContractType),
113+
datastore.ContractType(fqops.ContractType),
114+
datastore.ContractType(routerops.ContractType),
115+
datastore.ContractType(rmn_remote.ContractType),
116+
datastore.ContractType(rmn_proxy.ContractType),
117+
datastore.ContractType(token_admin_registry.ContractType),
118+
}
119+
120+
for _, ct := range allowedTypes {
121+
require.True(t, e.NeedsOwnershipCheck(datastore.AddressRef{Type: ct}), "expected allowed type %s to require ownership check", ct)
122+
}
123+
require.False(t, e.NeedsOwnershipCheck(datastore.AddressRef{Type: "UnknownType"}))
124+
}
125+
126+
func TestVerifyContractOwnership_NoRPCConfigured(t *testing.T) {
127+
e := &EVMContractOwnership{}
128+
err := e.VerifyContractOwnership(t.Context(), logger.Test(t), datastore.NewMemoryDataStore().Seal(), cfgnet.Network{
129+
ChainSelector: chainsel.ETHEREUM_MAINNET.Selector,
130+
RPCs: nil,
131+
}, nil)
132+
require.Error(t, err)
133+
require.ErrorContains(t, err, "has no HTTP RPC configured")
134+
}
135+
136+
func TestVerifyContractOwnership_InvalidPreferredURLScheme(t *testing.T) {
137+
e := &EVMContractOwnership{}
138+
err := e.VerifyContractOwnership(t.Context(), logger.Test(t), datastore.NewMemoryDataStore().Seal(), cfgnet.Network{
139+
ChainSelector: chainsel.ETHEREUM_MAINNET.Selector,
140+
RPCs: []cfgnet.RPC{{
141+
RPCName: "bad-rpc",
142+
HTTPURL: "http://localhost:8545",
143+
PreferredURLScheme: "definitely-invalid",
144+
}},
145+
}, nil)
146+
require.Error(t, err)
147+
require.ErrorContains(t, err, "invalid preferred URL scheme")
148+
}
149+
150+
func TestVerifyContractOwnership_DialRPCFailure(t *testing.T) {
151+
e := &EVMContractOwnership{}
152+
err := e.VerifyContractOwnership(t.Context(), logger.Test(t), datastore.NewMemoryDataStore().Seal(), cfgnet.Network{
153+
ChainSelector: chainsel.ETHEREUM_MAINNET.Selector,
154+
RPCs: []cfgnet.RPC{{
155+
RPCName: "local",
156+
HTTPURL: "http://localhost:8545",
157+
PreferredURLScheme: "http",
158+
}},
159+
}, nil)
160+
require.Error(t, err)
161+
require.ErrorContains(t, err, "dial RPC for chain")
162+
}

0 commit comments

Comments
 (0)