Skip to content

Commit 63fda69

Browse files
feat(mcms): query Timelock contract for CallProxy address (#489)
This PR fixes two issues: 1. the mcms CLI did not search for the CallProxy address in the DataStore, which meant domains that use _only_ the DataStore would see errors when executing a few subcommands. 2. we now query the timelock contract for the CallProxy address, which allows us to handle the case where multiple CallProxy contracts are in the DataStore for the same chain. [DX-1999](https://smartcontract-it.atlassian.net/browse/DX-1999) [DX-1999]: https://smartcontract-it.atlassian.net/browse/DX-1999?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent f4631e0 commit 63fda69

File tree

3 files changed

+303
-65
lines changed

3 files changed

+303
-65
lines changed

.changeset/clear-coats-judge.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": patch
3+
---
4+
5+
feat(mcms): query Timelock contract for CallProxy address

engine/cld/legacy/cli/mcmsv2/mcms_v2.go

Lines changed: 90 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import (
66
"crypto/rand"
77
"errors"
88
"fmt"
9+
"math/big"
910
"os"
1011
"slices"
1112
"strings"
1213
"time"
1314

15+
"github.com/ethereum/go-ethereum/accounts/abi/bind/v2"
1416
"github.com/ethereum/go-ethereum/common"
1517
gethtypes "github.com/ethereum/go-ethereum/core/types"
1618
"github.com/ethereum/go-ethereum/crypto"
@@ -32,6 +34,7 @@ import (
3234
"go.uber.org/zap/zapcore"
3335

3436
"github.com/smartcontractkit/chainlink-deployments-framework/chain"
37+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
3538
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
3639
cldf_chains "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/chains"
3740
cldf_config "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config"
@@ -563,40 +566,12 @@ func buildTimelockExecuteOperationV2Cmd(lggr logger.Logger, domain cldf_domain.D
563566
return fmt.Errorf("failed to create TimelockExecutable: %w", err)
564567
}
565568

566-
// Get AddressBook
567-
envdir := domain.EnvDir(cfgv2.envStr)
568-
ab, err := envdir.AddressBook()
569+
executeOptions, err := timelockExecuteOptions(cmd.Context(), lggr, domain, cfgv2)
569570
if err != nil {
570-
return fmt.Errorf("failed to load address book: %w", err)
571+
return fmt.Errorf("failed to get timelock execute options: %w", err)
571572
}
572573

573-
// Get Chain Contracts
574-
contracts, err := ab.AddressesForChain(cfgv2.chainSelector)
575-
if err != nil {
576-
return fmt.Errorf("failed to get contracts for chain %d: %w", cfgv2.chainSelector, err)
577-
}
578-
579-
// Get CallProxy address
580-
callProxyAddress := ""
581-
for address, contract := range contracts {
582-
if contract.Type == analyzer.CallProxy {
583-
// TODO: this assumes there is only one CallProxy per chain.
584-
// What happens if there are multiple? I think its safe to assume
585-
// there is only one for now but that might not always be the case.
586-
// Maybe we can do a check on the timelock to see if the found CallProxy
587-
// has the executor role?
588-
callProxyAddress = address
589-
break
590-
}
591-
}
592-
593-
// If there is no CallProxy, we don't need to pass it to the executor
594-
opts := []mcms.Option{}
595-
if callProxyAddress != "" {
596-
opts = append(opts, mcms.WithCallProxy(callProxyAddress))
597-
}
598-
599-
result, err := executable.Execute(context.Background(), index, opts...)
574+
result, err := executable.Execute(cmd.Context(), index, executeOptions...)
600575
if err != nil {
601576
return fmt.Errorf("failed to execute operation %d: %w", index, err)
602577
}
@@ -1267,37 +1242,9 @@ func timelockExecuteChainCommand(ctx context.Context, lggr logger.Logger, cfg *c
12671242
return fmt.Errorf("failed to create TimelockExecutable: %w", err)
12681243
}
12691244

1270-
// Get AddressBook
1271-
envdir := domain.EnvDir(cfg.envStr)
1272-
ab, err := envdir.AddressBook()
1245+
executeOptions, err := timelockExecuteOptions(ctx, lggr, domain, cfg)
12731246
if err != nil {
1274-
return fmt.Errorf("failed to load address book: %w", err)
1275-
}
1276-
1277-
// Get Chain Contracts
1278-
contracts, err := ab.AddressesForChain(cfg.chainSelector)
1279-
if err != nil {
1280-
return fmt.Errorf("failed to get contracts for chain %d: %w", cfg.chainSelector, err)
1281-
}
1282-
1283-
// Get CallProxy address
1284-
callProxyAddress := ""
1285-
for address, contract := range contracts {
1286-
if contract.Type == analyzer.CallProxy {
1287-
// TODO: this assumes there is only one CallProxy per chain.
1288-
// What happens if there are multiple? I think its safe to assume
1289-
// there is only one for now but that might not always be the case.
1290-
// Maybe we can do a check on the timelock to see if the found CallProxy
1291-
// has the executor role?
1292-
callProxyAddress = address
1293-
break
1294-
}
1295-
}
1296-
1297-
// If there is no CallProxy, we don't need to pass it to the executor
1298-
opts := []mcms.Option{}
1299-
if callProxyAddress != "" {
1300-
opts = append(opts, mcms.WithCallProxy(callProxyAddress))
1247+
return fmt.Errorf("failed to get timelock execute options: %w", err)
13011248
}
13021249

13031250
for i := range cfg.timelockProposal.Operations {
@@ -1312,7 +1259,7 @@ func timelockExecuteChainCommand(ctx context.Context, lggr logger.Logger, cfg *c
13121259
return fmt.Errorf("operation %d is not ready to be executed: %w", i, err)
13131260
}
13141261

1315-
result, err := executable.Execute(ctx, i, opts...)
1262+
result, err := executable.Execute(ctx, i, executeOptions...)
13161263
if err != nil {
13171264
return fmt.Errorf("failed to execute operation %d: %w", i, err)
13181265
}
@@ -1569,3 +1516,84 @@ func getProposalSigners(
15691516

15701517
return addresses, nil
15711518
}
1519+
1520+
func timelockExecuteOptions(
1521+
ctx context.Context, lggr logger.Logger, _ cldf_domain.Domain, cfg *cfgv2,
1522+
) ([]mcms.Option, error) {
1523+
options := []mcms.Option{}
1524+
1525+
family, err := chainsel.GetSelectorFamily(cfg.chainSelector)
1526+
if err != nil {
1527+
return nil, fmt.Errorf("failed to get selector family: %w", err)
1528+
}
1529+
if family == chainsel.FamilyEVM {
1530+
err := addCallProxyOption(ctx, lggr, cfg, &options)
1531+
if err != nil {
1532+
return options, fmt.Errorf("failed to add CallProxy option: %w", err)
1533+
}
1534+
}
1535+
1536+
return options, nil
1537+
}
1538+
1539+
func addCallProxyOption(
1540+
ctx context.Context, lggr logger.Logger, cfg *cfgv2, options *[]mcms.Option,
1541+
) error {
1542+
timelockAddress, ok := cfg.timelockProposal.TimelockAddresses[types.ChainSelector(cfg.chainSelector)]
1543+
if !ok {
1544+
return fmt.Errorf("failed to find timelock address for chain selector %d", cfg.chainSelector)
1545+
}
1546+
1547+
chain, ok := cfg.blockchains.EVMChains()[cfg.chainSelector]
1548+
if !ok {
1549+
return fmt.Errorf("failed to find evm chain for selector %d", cfg.chainSelector)
1550+
}
1551+
1552+
timelockContract, err := bindings.NewRBACTimelock(common.HexToAddress(timelockAddress), chain.Client)
1553+
if err != nil {
1554+
return fmt.Errorf("failed to create timelock contract with address %v: %w", timelockAddress, err)
1555+
}
1556+
1557+
callOpts := &bind.CallOpts{Context: ctx}
1558+
1559+
role, err := timelockContract.EXECUTORROLE(callOpts)
1560+
if err != nil {
1561+
return fmt.Errorf("failed to get executor role from timelock contract: %w", err)
1562+
}
1563+
memberCount, err := timelockContract.GetRoleMemberCount(callOpts, role)
1564+
if err != nil {
1565+
return fmt.Errorf("failed to get executor member count from timelock contract: %w", err)
1566+
}
1567+
for i := range memberCount.Int64() {
1568+
executorAddress, ierr := timelockContract.GetRoleMember(callOpts, role, big.NewInt(i))
1569+
if ierr != nil {
1570+
return fmt.Errorf("failed to get executor address from timelock contract: %w", ierr)
1571+
}
1572+
1573+
// search for executor address in the datastore
1574+
callProxyRefs := cfg.env.DataStore.Addresses().Filter(
1575+
datastore.AddressRefByAddress(executorAddress.Hex()),
1576+
datastore.AddressRefByChainSelector(cfg.chainSelector),
1577+
datastore.AddressRefByType("CallProxy"))
1578+
1579+
if len(callProxyRefs) > 0 {
1580+
*options = append(*options, mcms.WithCallProxy(executorAddress.Hex()))
1581+
return nil
1582+
}
1583+
1584+
// if not found, search in the addressbook
1585+
addressesForChain, ierr := cfg.env.ExistingAddresses.AddressesForChain(cfg.chainSelector) //nolint:staticcheck
1586+
if ierr != nil {
1587+
lggr.Infof("unable to get addresses for chain %d in addressbook: %s", cfg.chainSelector, ierr.Error())
1588+
continue // ignore error; some domains don't use the addressbook anymore
1589+
}
1590+
for address, typeAndVersion := range addressesForChain {
1591+
if address == executorAddress.Hex() && typeAndVersion.Type == "CallProxy" {
1592+
*options = append(*options, mcms.WithCallProxy(executorAddress.Hex()))
1593+
return nil
1594+
}
1595+
}
1596+
}
1597+
1598+
return fmt.Errorf("failed to find call proxy contract for timelock %v", timelockAddress)
1599+
}

0 commit comments

Comments
 (0)