|
| 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 | +} |
0 commit comments