diff --git a/deployment/ccip/changeset/active_candidate_test.go b/deployment/ccip/changeset/active_candidate_test.go index 9daf383c971..9386ca9f93d 100644 --- a/deployment/ccip/changeset/active_candidate_test.go +++ b/deployment/ccip/changeset/active_candidate_test.go @@ -19,6 +19,7 @@ import ( jobv1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" ccdeploy "github.com/smartcontractkit/chainlink/deployment/ccip" + commondeploy "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -43,7 +44,6 @@ func TestActiveCandidate(t *testing.T) { FeedChainSel: tenv.FeedChainSel, ChainsToDeploy: tenv.Env.AllChainSelectors(), TokenConfig: tokenConfig, - MCMSConfig: ccdeploy.NewTestMCMSConfig(t, e), OCRSecrets: deployment.XXXGenerateTestOCRSecrets(), }) require.NoError(t, err) @@ -114,9 +114,9 @@ func TestActiveCandidate(t *testing.T) { ccdeploy.TransferAllOwnership(t, state, homeCS, e) acceptOwnershipProposal, err := ccdeploy.GenerateAcceptOwnershipProposal(state, homeCS, e.AllChainSelectors()) require.NoError(t, err) - acceptOwnershipExec := ccdeploy.SignProposal(t, e, acceptOwnershipProposal) + acceptOwnershipExec := commondeploy.SignProposal(t, e, acceptOwnershipProposal) for _, sel := range e.AllChainSelectors() { - ccdeploy.ExecuteProposal(t, e, acceptOwnershipExec, state, sel) + commondeploy.ExecuteProposal(t, e, acceptOwnershipExec, state.Chains[sel].Timelock, sel) } // Apply the accept ownership proposal to all the chains. @@ -176,8 +176,8 @@ func TestActiveCandidate(t *testing.T) { Batch: setCommitCandidateOp, }}, "set new candidates on commit plugin", 0) require.NoError(t, err) - setCommitCandidateSigned := ccdeploy.SignProposal(t, e, setCommitCandidateProposal) - ccdeploy.ExecuteProposal(t, e, setCommitCandidateSigned, state, homeCS) + setCommitCandidateSigned := commondeploy.SignProposal(t, e, setCommitCandidateProposal) + commondeploy.ExecuteProposal(t, e, setCommitCandidateSigned, state.Chains[homeCS].Timelock, homeCS) // create the op for the commit plugin as well setExecCandidateOp, err := ccdeploy.SetCandidateOnExistingDon( @@ -194,8 +194,8 @@ func TestActiveCandidate(t *testing.T) { Batch: setExecCandidateOp, }}, "set new candidates on commit and exec plugins", 0) require.NoError(t, err) - setExecCandidateSigned := ccdeploy.SignProposal(t, e, setExecCandidateProposal) - ccdeploy.ExecuteProposal(t, e, setExecCandidateSigned, state, homeCS) + setExecCandidateSigned := commondeploy.SignProposal(t, e, setExecCandidateProposal) + commondeploy.ExecuteProposal(t, e, setExecCandidateSigned, state.Chains[homeCS].Timelock, homeCS) // check setup was successful by confirming number of nodes from cap reg donInfo, err = state.Chains[homeCS].CapabilityRegistry.GetDON(nil, donID) @@ -221,8 +221,8 @@ func TestActiveCandidate(t *testing.T) { Batch: promoteOps, }}, "promote candidates and revoke actives", 0) require.NoError(t, err) - promoteSigned := ccdeploy.SignProposal(t, e, promoteProposal) - ccdeploy.ExecuteProposal(t, e, promoteSigned, state, homeCS) + promoteSigned := commondeploy.SignProposal(t, e, promoteProposal) + commondeploy.ExecuteProposal(t, e, promoteSigned, state.Chains[homeCS].Timelock, homeCS) // [NEW ACTIVE, NO CANDIDATE] done promoting // [NEW ACTIVE, NO CANDIDATE] check onchain state diff --git a/deployment/ccip/changeset/add_chain_test.go b/deployment/ccip/changeset/add_chain_test.go index 6a87bdd0a0a..69f66acb730 100644 --- a/deployment/ccip/changeset/add_chain_test.go +++ b/deployment/ccip/changeset/add_chain_test.go @@ -5,6 +5,7 @@ import ( "time" ccipdeployment "github.com/smartcontractkit/chainlink/deployment/ccip" + commondeployment "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" @@ -44,7 +45,6 @@ func TestAddChainInbound(t *testing.T) { FeedChainSel: e.FeedChainSel, ChainsToDeploy: initialDeploy, TokenConfig: tokenConfig, - MCMSConfig: ccipdeployment.NewTestMCMSConfig(t, e.Env), OCRSecrets: deployment.XXXGenerateTestOCRSecrets(), }) require.NoError(t, err) @@ -74,7 +74,7 @@ func TestAddChainInbound(t *testing.T) { ccipdeployment.FeeTokenContracts{ LinkToken: state.Chains[newChain].LinkToken, Weth9: state.Chains[newChain].Weth9, - }, ccipdeployment.NewTestMCMSConfig(t, e.Env), rmnHome) + }, rmnHome) require.NoError(t, err) require.NoError(t, e.Env.ExistingAddresses.Merge(newChainAddresses)) state, err = ccipdeployment.LoadOnchainState(e.Env) @@ -112,10 +112,10 @@ func TestAddChainInbound(t *testing.T) { acceptOwnershipProposal, err := ccipdeployment.GenerateAcceptOwnershipProposal(state, e.HomeChainSel, initialDeploy) require.NoError(t, err) - acceptOwnershipExec := ccipdeployment.SignProposal(t, e.Env, acceptOwnershipProposal) + acceptOwnershipExec := commondeployment.SignProposal(t, e.Env, acceptOwnershipProposal) // Apply the accept ownership proposal to all the chains. for _, sel := range initialDeploy { - ccipdeployment.ExecuteProposal(t, e.Env, acceptOwnershipExec, state, sel) + commondeployment.ExecuteProposal(t, e.Env, acceptOwnershipExec, state.Chains[sel].Timelock, sel) } for _, chain := range initialDeploy { owner, err2 := state.Chains[chain].OnRamp.Owner(nil) diff --git a/deployment/ccip/changeset/home_chain.go b/deployment/ccip/changeset/home_chain.go index 7d7f64a8bb8..0fabd2efb18 100644 --- a/deployment/ccip/changeset/home_chain.go +++ b/deployment/ccip/changeset/home_chain.go @@ -17,7 +17,6 @@ var _ deployment.ChangeSet[DeployHomeChainConfig] = DeployHomeChain // DeployHomeChain is a separate changeset because it is a standalone deployment performed once in home chain for the entire CCIP deployment. func DeployHomeChain(env deployment.Environment, cfg DeployHomeChainConfig) (deployment.ChangesetOutput, error) { - err := cfg.Validate() if err != nil { return deployment.ChangesetOutput{}, errors.Wrapf(deployment.ErrInvalidConfig, "%v", err) diff --git a/deployment/ccip/changeset/initial_deploy_test.go b/deployment/ccip/changeset/initial_deploy_test.go index b7dbdfcc972..75df27edb5f 100644 --- a/deployment/ccip/changeset/initial_deploy_test.go +++ b/deployment/ccip/changeset/initial_deploy_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink/deployment" ccdeploy "github.com/smartcontractkit/chainlink/deployment/ccip" @@ -33,7 +34,6 @@ func TestInitialDeploy(t *testing.T) { FeedChainSel: tenv.FeedChainSel, ChainsToDeploy: tenv.Env.AllChainSelectors(), TokenConfig: ccdeploy.NewTestTokenConfig(state.Chains[tenv.FeedChainSel].USDFeeds), - MCMSConfig: ccdeploy.NewTestMCMSConfig(t, e), OCRSecrets: deployment.XXXGenerateTestOCRSecrets(), }) require.NoError(t, err) diff --git a/deployment/ccip/deploy.go b/deployment/ccip/deploy.go index 8a33cf0d26d..77acd61a147 100644 --- a/deployment/ccip/deploy.go +++ b/deployment/ccip/deploy.go @@ -7,11 +7,8 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/smartcontractkit/ccip-owner-contracts/pkg/config" - owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/registry_module_owner_custom" @@ -65,9 +62,6 @@ type DeployCCIPContractConfig struct { FeedChainSel uint64 ChainsToDeploy []uint64 TokenConfig TokenConfig - // I believe it makes sense to have the same signers across all chains - // since that's the point MCMS. - MCMSConfig MCMSConfig // For setting OCR configuration OCRSecrets deployment.OCRSecrets } @@ -140,7 +134,7 @@ func DeployCCIPContracts(e deployment.Environment, ab deployment.AddressBook, c err = DeployChainContracts(e, chain, ab, FeeTokenContracts{ LinkToken: existingState.Chains[chainSel].LinkToken, Weth9: existingState.Chains[chainSel].Weth9, - }, c.MCMSConfig, rmnHome) + }, rmnHome) if err != nil { return err } @@ -189,113 +183,6 @@ func DeployCCIPContracts(e deployment.Environment, ab deployment.AddressBook, c return nil } -type MCMSConfig struct { - Admin config.Config - Canceller config.Config - Bypasser config.Config - Proposer config.Config - Executors []common.Address -} - -func DeployMCMSWithConfig( - contractType deployment.ContractType, - lggr logger.Logger, - chain deployment.Chain, - ab deployment.AddressBook, - mcmConfig config.Config, -) (*deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig], error) { - groupQuorums, groupParents, signerAddresses, signerGroups := mcmConfig.ExtractSetConfigInputs() - mcm, err := deployment.DeployContract(lggr, chain, ab, - func(chain deployment.Chain) deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig] { - mcmAddr, tx, mcm, err2 := owner_helpers.DeployManyChainMultiSig( - chain.DeployerKey, - chain.Client, - ) - return deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig]{ - mcmAddr, mcm, tx, deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0), err2, - } - }) - if err != nil { - lggr.Errorw("Failed to deploy mcm", "err", err) - return mcm, err - } - mcmsTx, err := mcm.Contract.SetConfig(chain.DeployerKey, - signerAddresses, - signerGroups, // Signer 1 is int group 0 (root group) with quorum 1. - groupQuorums, - groupParents, - false, - ) - if _, err := deployment.ConfirmIfNoError(chain, mcmsTx, err); err != nil { - lggr.Errorw("Failed to confirm mcm config", "err", err) - return mcm, err - } - return mcm, nil -} - -type MCMSContracts struct { - Admin *deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig] - Canceller *deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig] - Bypasser *deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig] - Proposer *deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig] - Timelock *deployment.ContractDeploy[*owner_helpers.RBACTimelock] -} - -// DeployMCMSContracts deploys the MCMS contracts for the given configuration -// as well as the timelock. -func DeployMCMSContracts( - lggr logger.Logger, - chain deployment.Chain, - ab deployment.AddressBook, - mcmConfig MCMSConfig, -) (*MCMSContracts, error) { - adminMCM, err := DeployMCMSWithConfig(AdminManyChainMultisig, lggr, chain, ab, mcmConfig.Admin) - if err != nil { - return nil, err - } - bypasser, err := DeployMCMSWithConfig(BypasserManyChainMultisig, lggr, chain, ab, mcmConfig.Bypasser) - if err != nil { - return nil, err - } - canceller, err := DeployMCMSWithConfig(CancellerManyChainMultisig, lggr, chain, ab, mcmConfig.Canceller) - if err != nil { - return nil, err - } - proposer, err := DeployMCMSWithConfig(ProposerManyChainMultisig, lggr, chain, ab, mcmConfig.Proposer) - if err != nil { - return nil, err - } - - timelock, err := deployment.DeployContract(lggr, chain, ab, - func(chain deployment.Chain) deployment.ContractDeploy[*owner_helpers.RBACTimelock] { - timelock, tx2, cc, err2 := owner_helpers.DeployRBACTimelock( - chain.DeployerKey, - chain.Client, - big.NewInt(0), // minDelay - adminMCM.Address, - []common.Address{proposer.Address}, // proposers - mcmConfig.Executors, //executors - []common.Address{canceller.Address}, // cancellers - []common.Address{bypasser.Address}, // bypassers - ) - return deployment.ContractDeploy[*owner_helpers.RBACTimelock]{ - timelock, cc, tx2, deployment.NewTypeAndVersion(RBACTimelock, deployment.Version1_0_0), err2, - } - }) - if err != nil { - lggr.Errorw("Failed to deploy timelock", "err", err) - return nil, err - } - lggr.Infow("deployed timelock", "addr", timelock.Address) - return &MCMSContracts{ - Admin: adminMCM, - Canceller: canceller, - Bypasser: bypasser, - Proposer: proposer, - Timelock: timelock, - }, nil -} - func DeployFeeTokensToChains(lggr logger.Logger, ab deployment.AddressBook, chains map[uint64]deployment.Chain) error { for _, chain := range chains { _, err := DeployFeeTokens(lggr, chain, ab) @@ -360,11 +247,16 @@ func DeployChainContracts( chain deployment.Chain, ab deployment.AddressBook, contractConfig FeeTokenContracts, - mcmsConfig MCMSConfig, rmnHome *rmn_home.RMNHome, ) error { - mcmsContracts, err := DeployMCMSContracts(e.Logger, chain, ab, mcmsConfig) + chainAddresses, err := e.ExistingAddresses.AddressesForChain(chain.Selector) + if err != nil { + e.Logger.Errorw("Failed to get chain addresses", "err", err) + return err + } + state, err := LoadChainState(chain, chainAddresses) if err != nil { + e.Logger.Errorw("Failed to load chain state", "err", err) return err } ccipReceiver, err := deployment.DeployContract(e.Logger, chain, ab, @@ -546,7 +438,7 @@ func DeployChainContracts( LinkToken: contractConfig.LinkToken.Address(), TokenPriceStalenessThreshold: uint32(24 * 60 * 60), }, - []common.Address{mcmsContracts.Timelock.Address}, // timelock should be able to update, ramps added after + []common.Address{state.Timelock.Address()}, // timelock should be able to update, ramps added after []common.Address{contractConfig.Weth9.Address(), contractConfig.LinkToken.Address()}, // fee tokens []fee_quoter.FeeQuoterTokenPriceFeedUpdate{}, []fee_quoter.FeeQuoterTokenTransferFeeConfigArgs{}, // TODO: tokens diff --git a/deployment/ccip/deploy_test.go b/deployment/ccip/deploy_test.go index 63aeacb4bdf..5842af25303 100644 --- a/deployment/ccip/deploy_test.go +++ b/deployment/ccip/deploy_test.go @@ -50,7 +50,6 @@ func TestDeployCCIPContracts(t *testing.T) { FeedChainSel: feedChainSel, ChainsToDeploy: e.AllChainSelectors(), TokenConfig: NewTokenConfig(), - MCMSConfig: NewTestMCMSConfig(t, e), OCRSecrets: deployment.XXXGenerateTestOCRSecrets(), }) require.NoError(t, err) diff --git a/deployment/ccip/propose.go b/deployment/ccip/propose.go index 9d6ac417968..d7baf9ab542 100644 --- a/deployment/ccip/propose.go +++ b/deployment/ccip/propose.go @@ -1,138 +1,19 @@ package ccipdeployment import ( - "bytes" - "context" - "crypto/ecdsa" "fmt" "math/big" - "testing" "time" - "github.com/ethereum/go-ethereum/accounts/abi/bind" + mapset "github.com/deckarep/golang-set/v2" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/smartcontractkit/ccip-owner-contracts/pkg/config" - owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms" "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" chainsel "github.com/smartcontractkit/chain-selectors" - "github.com/stretchr/testify/require" - - mapset "github.com/deckarep/golang-set/v2" "github.com/smartcontractkit/chainlink/deployment" ) -var ( - TestXXXMCMSSigner *ecdsa.PrivateKey -) - -func init() { - key, err := crypto.GenerateKey() - if err != nil { - panic(err) - } - TestXXXMCMSSigner = key -} - -func SingleGroupMCMS(t *testing.T) config.Config { - publicKey := TestXXXMCMSSigner.Public().(*ecdsa.PublicKey) - // Convert the public key to an Ethereum address - address := crypto.PubkeyToAddress(*publicKey) - c, err := config.NewConfig(1, []common.Address{address}, []config.Config{}) - require.NoError(t, err) - return *c -} - -func NewTestMCMSConfig(t *testing.T, e deployment.Environment) MCMSConfig { - c := SingleGroupMCMS(t) - // All deployer keys can execute. - var executors []common.Address - for _, chain := range e.Chains { - executors = append(executors, chain.DeployerKey.From) - } - return MCMSConfig{ - Admin: c, - Bypasser: c, - Canceller: c, - Executors: executors, - Proposer: c, - } -} - -func SignProposal(t *testing.T, env deployment.Environment, proposal *timelock.MCMSWithTimelockProposal) *mcms.Executor { - executorClients := make(map[mcms.ChainIdentifier]mcms.ContractDeployBackend) - for _, chain := range env.Chains { - chainselc, exists := chainsel.ChainBySelector(chain.Selector) - require.True(t, exists) - chainSel := mcms.ChainIdentifier(chainselc.Selector) - executorClients[chainSel] = chain.Client - } - executor, err := proposal.ToExecutor(true) - require.NoError(t, err) - payload, err := executor.SigningHash() - require.NoError(t, err) - // Sign the payload - sig, err := crypto.Sign(payload.Bytes(), TestXXXMCMSSigner) - require.NoError(t, err) - mcmSig, err := mcms.NewSignatureFromBytes(sig) - require.NoError(t, err) - executor.Proposal.AddSignature(mcmSig) - require.NoError(t, executor.Proposal.Validate()) - return executor -} - -func ExecuteProposal(t *testing.T, env deployment.Environment, executor *mcms.Executor, - state CCIPOnChainState, sel uint64) { - t.Log("Executing proposal on chain", sel) - // Set the root. - tx, err2 := executor.SetRootOnChain(env.Chains[sel].Client, env.Chains[sel].DeployerKey, mcms.ChainIdentifier(sel)) - if err2 != nil { - require.NoError(t, deployment.MaybeDataErr(err2)) - } - _, err2 = env.Chains[sel].Confirm(tx) - require.NoError(t, err2) - - // TODO: This sort of helper probably should move to the MCMS lib. - // Execute all the transactions in the proposal which are for this chain. - for _, chainOp := range executor.Operations[mcms.ChainIdentifier(sel)] { - for idx, op := range executor.ChainAgnosticOps { - if bytes.Equal(op.Data, chainOp.Data) && op.To == chainOp.To { - opTx, err3 := executor.ExecuteOnChain(env.Chains[sel].Client, env.Chains[sel].DeployerKey, idx) - require.NoError(t, err3) - block, err3 := env.Chains[sel].Confirm(opTx) - require.NoError(t, err3) - t.Log("executed", chainOp) - it, err3 := state.Chains[sel].Timelock.FilterCallScheduled(&bind.FilterOpts{ - Start: block, - End: &block, - Context: context.Background(), - }, nil, nil) - require.NoError(t, err3) - var calls []owner_helpers.RBACTimelockCall - var pred, salt [32]byte - for it.Next() { - // Note these are the same for the whole batch, can overwrite - pred = it.Event.Predecessor - salt = it.Event.Salt - t.Log("scheduled", it.Event) - calls = append(calls, owner_helpers.RBACTimelockCall{ - Target: it.Event.Target, - Data: it.Event.Data, - Value: it.Event.Value, - }) - } - tx, err := state.Chains[sel].Timelock.ExecuteBatch( - env.Chains[sel].DeployerKey, calls, pred, salt) - require.NoError(t, err) - _, err = env.Chains[sel].Confirm(tx) - require.NoError(t, err) - } - } - } -} - func GenerateAcceptOwnershipProposal( state CCIPOnChainState, homeChain uint64, diff --git a/deployment/ccip/state.go b/deployment/ccip/state.go index 650f46d2b3a..271eaa716f0 100644 --- a/deployment/ccip/state.go +++ b/deployment/ccip/state.go @@ -14,6 +14,7 @@ import ( "github.com/smartcontractkit/chainlink/deployment/ccip/view/v1_2" "github.com/smartcontractkit/chainlink/deployment/ccip/view/v1_5" "github.com/smartcontractkit/chainlink/deployment/ccip/view/v1_6" + commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" common_v1_0 "github.com/smartcontractkit/chainlink/deployment/common/view/v1_0" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" @@ -26,8 +27,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/burn_mint_erc677" - owner_wrappers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/nonce_manager" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/onramp" @@ -42,6 +41,7 @@ import ( // CCIPChainState holds a Go binding for all the currently deployed CCIP contracts // on a chain. If a binding is nil, it means here is no such contract on the chain. type CCIPChainState struct { + commoncs.MCMSWithTimelockState OnRamp *onramp.OnRamp OffRamp *offramp.OffRamp FeeQuoter *fee_quoter.FeeQuoter @@ -68,11 +68,6 @@ type CCIPChainState struct { CapabilityRegistry *capabilities_registry.CapabilitiesRegistry CCIPHome *ccip_home.CCIPHome RMNHome *rmn_home.RMNHome - AdminMcm *owner_wrappers.ManyChainMultiSig - BypasserMcm *owner_wrappers.ManyChainMultiSig - CancellerMcm *owner_wrappers.ManyChainMultiSig - ProposerMcm *owner_wrappers.ManyChainMultiSig - Timelock *owner_wrappers.RBACTimelock // TODO remove once staging upgraded. CCIPConfig *ccip_config.CCIPConfig @@ -164,6 +159,13 @@ func (c CCIPChainState) GenerateView() (view.ChainView, error) { } chainView.CapabilityRegistry[c.CapabilityRegistry.Address().Hex()] = capRegView } + if c.MCMSWithTimelockState.Timelock != nil { + mcmsView, err := c.MCMSWithTimelockState.GenerateMCMSWithTimelockView() + if err != nil { + return chainView, err + } + chainView.MCMSWithTimelock = mcmsView + } return chainView, nil } @@ -229,38 +231,13 @@ func LoadOnchainState(e deployment.Environment) (CCIPOnChainState, error) { // Modifies map in place func LoadChainState(chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (CCIPChainState, error) { var state CCIPChainState + mcmsWithTimelock, err := commoncs.LoadMCMSWithTimelockState(chain, addresses) + if err != nil { + return state, err + } + state.MCMSWithTimelockState = *mcmsWithTimelock for address, tvStr := range addresses { switch tvStr.String() { - case deployment.NewTypeAndVersion(RBACTimelock, deployment.Version1_0_0).String(): - tl, err := owner_wrappers.NewRBACTimelock(common.HexToAddress(address), chain.Client) - if err != nil { - return state, err - } - state.Timelock = tl - case deployment.NewTypeAndVersion(AdminManyChainMultisig, deployment.Version1_0_0).String(): - mcms, err := owner_wrappers.NewManyChainMultiSig(common.HexToAddress(address), chain.Client) - if err != nil { - return state, err - } - state.AdminMcm = mcms - case deployment.NewTypeAndVersion(ProposerManyChainMultisig, deployment.Version1_0_0).String(): - mcms, err := owner_wrappers.NewManyChainMultiSig(common.HexToAddress(address), chain.Client) - if err != nil { - return state, err - } - state.ProposerMcm = mcms - case deployment.NewTypeAndVersion(BypasserManyChainMultisig, deployment.Version1_0_0).String(): - mcms, err := owner_wrappers.NewManyChainMultiSig(common.HexToAddress(address), chain.Client) - if err != nil { - return state, err - } - state.BypasserMcm = mcms - case deployment.NewTypeAndVersion(CancellerManyChainMultisig, deployment.Version1_0_0).String(): - mcms, err := owner_wrappers.NewManyChainMultiSig(common.HexToAddress(address), chain.Client) - if err != nil { - return state, err - } - state.CancellerMcm = mcms case deployment.NewTypeAndVersion(CapabilitiesRegistry, deployment.Version1_0_0).String(): cr, err := capabilities_registry.NewCapabilitiesRegistry(common.HexToAddress(address), chain.Client) if err != nil { diff --git a/deployment/ccip/view/types/contract_state.go b/deployment/ccip/view/types/contract_state.go deleted file mode 100644 index f65c510af53..00000000000 --- a/deployment/ccip/view/types/contract_state.go +++ /dev/null @@ -1,33 +0,0 @@ -package types - -import ( - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" -) - -type ContractMetaData struct { - TypeAndVersion string `json:"typeAndVersion,omitempty"` - Address common.Address `json:"address,omitempty"` - Owner common.Address `json:"owner,omitempty"` -} - -func NewContractMetaData(tv Meta, addr common.Address) (ContractMetaData, error) { - tvStr, err := tv.TypeAndVersion(nil) - if err != nil { - return ContractMetaData{}, err - } - owner, err := tv.Owner(nil) - if err != nil { - return ContractMetaData{}, err - } - return ContractMetaData{ - TypeAndVersion: tvStr, - Address: addr, - Owner: owner, - }, nil -} - -type Meta interface { - TypeAndVersion(opts *bind.CallOpts) (string, error) - Owner(opts *bind.CallOpts) (common.Address, error) -} diff --git a/deployment/ccip/view/view.go b/deployment/ccip/view/view.go index 9ef8583bdf6..318e09100b9 100644 --- a/deployment/ccip/view/view.go +++ b/deployment/ccip/view/view.go @@ -26,6 +26,7 @@ type ChainView struct { OnRamp map[string]v1_6.OnRampView `json:"onRamp,omitempty"` OffRamp map[string]v1_6.OffRampView `json:"offRamp,omitempty"` CapabilityRegistry map[string]common_v1_0.CapabilityRegistryView `json:"capabilityRegistry,omitempty"` + MCMSWithTimelock common_v1_0.MCMSWithTimelockView `json:"mcmsWithTimelock,omitempty"` } func NewChain() ChainView { @@ -44,6 +45,7 @@ func NewChain() ChainView { OnRamp: make(map[string]v1_6.OnRampView), OffRamp: make(map[string]v1_6.OffRampView), CapabilityRegistry: make(map[string]common_v1_0.CapabilityRegistryView), + MCMSWithTimelock: common_v1_0.MCMSWithTimelockView{}, } } diff --git a/deployment/common/changeset/deploy_mcms_with_timelock.go b/deployment/common/changeset/deploy_mcms_with_timelock.go new file mode 100644 index 00000000000..88caea1c125 --- /dev/null +++ b/deployment/common/changeset/deploy_mcms_with_timelock.go @@ -0,0 +1,32 @@ +package changeset + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/config" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/changeset/internal" +) + +var _ deployment.ChangeSet[map[uint64]MCMSWithTimelockConfig] = DeployMCMSWithTimelock + +type MCMSWithTimelockConfig struct { + Canceller config.Config + Bypasser config.Config + Proposer config.Config + TimelockExecutors []common.Address + TimelockMinDelay *big.Int +} + +func DeployMCMSWithTimelock(e deployment.Environment, cfgByChain map[uint64]MCMSWithTimelockConfig) (deployment.ChangesetOutput, error) { + newAddresses := deployment.NewMemoryAddressBook() + err := internal.DeployMCMSWithTimelockContractsBatch( + e.Logger, e.Chains, newAddresses, cfgByChain, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + return deployment.ChangesetOutput{AddressBook: newAddresses}, nil +} diff --git a/deployment/common/changeset/internal/mcms.go b/deployment/common/changeset/internal/mcms.go new file mode 100644 index 00000000000..cf7ef95e629 --- /dev/null +++ b/deployment/common/changeset/internal/mcms.go @@ -0,0 +1,138 @@ +package internal + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/config" + owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/common/view/v1_0" +) + +func DeployMCMSWithConfig( + contractType deployment.ContractType, + lggr logger.Logger, + chain deployment.Chain, + ab deployment.AddressBook, + mcmConfig config.Config, +) (*deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig], error) { + groupQuorums, groupParents, signerAddresses, signerGroups := mcmConfig.ExtractSetConfigInputs() + mcm, err := deployment.DeployContract[*owner_helpers.ManyChainMultiSig](lggr, chain, ab, + func(chain deployment.Chain) deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig] { + mcmAddr, tx, mcm, err2 := owner_helpers.DeployManyChainMultiSig( + chain.DeployerKey, + chain.Client, + ) + return deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig]{ + mcmAddr, mcm, tx, deployment.NewTypeAndVersion(contractType, deployment.Version1_0_0), err2, + } + }) + if err != nil { + lggr.Errorw("Failed to deploy mcm", "err", err) + return mcm, err + } + mcmsTx, err := mcm.Contract.SetConfig(chain.DeployerKey, + signerAddresses, + // Signer 1 is int group 0 (root group) with quorum 1. + signerGroups, + groupQuorums, + groupParents, + false, + ) + if _, err := deployment.ConfirmIfNoError(chain, mcmsTx, err); err != nil { + lggr.Errorw("Failed to confirm mcm config", "err", err) + return mcm, err + } + return mcm, nil +} + +// MCMSWithTimelockDeploy holds a bundle of MCMS contract deploys. +type MCMSWithTimelockDeploy struct { + Canceller *deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig] + Bypasser *deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig] + Proposer *deployment.ContractDeploy[*owner_helpers.ManyChainMultiSig] + Timelock *deployment.ContractDeploy[*owner_helpers.RBACTimelock] +} + +func DeployMCMSWithTimelockContractsBatch( + lggr logger.Logger, + chains map[uint64]deployment.Chain, + ab deployment.AddressBook, + cfgByChain map[uint64]changeset.MCMSWithTimelockConfig, +) error { + for chainSel, cfg := range cfgByChain { + _, err := DeployMCMSWithTimelockContracts(lggr, chains[chainSel], ab, cfg) + if err != nil { + return err + } + } + return nil +} + +// DeployMCMSWithTimelockContracts deploys an MCMS for +// each of the timelock roles Bypasser, ProposerMcm, Canceller. +// MCMS contracts for the given configuration +// as well as the timelock. It's not necessarily the only way to use +// the timelock and MCMS, but its reasonable pattern. +func DeployMCMSWithTimelockContracts( + lggr logger.Logger, + chain deployment.Chain, + ab deployment.AddressBook, + config types.MCMSWithTimelockConfig, +) (*MCMSWithTimelockDeploy, error) { + bypasser, err := DeployMCMSWithConfig(BypasserManyChainMultisig, lggr, chain, ab, config.Bypasser) + if err != nil { + return nil, err + } + canceller, err := DeployMCMSWithConfig(CancellerManyChainMultisig, lggr, chain, ab, config.Canceller) + if err != nil { + return nil, err + } + proposer, err := DeployMCMSWithConfig(ProposerManyChainMultisig, lggr, chain, ab, config.Proposer) + if err != nil { + return nil, err + } + + timelock, err := deployment.DeployContract[*owner_helpers.RBACTimelock](lggr, chain, ab, + func(chain deployment.Chain) deployment.ContractDeploy[*owner_helpers.RBACTimelock] { + timelock, tx2, cc, err2 := owner_helpers.DeployRBACTimelock( + chain.DeployerKey, + chain.Client, + config.TimelockMinDelay, + // Deployer is the initial admin. + // TODO: Could expose this as config? + // Or keep this enforced to follow the same pattern? + chain.DeployerKey.From, + []common.Address{proposer.Address}, // proposers + config.TimelockExecutors, //executors + []common.Address{canceller.Address}, // cancellers + []common.Address{bypasser.Address}, // bypassers + ) + return deployment.ContractDeploy[*owner_helpers.RBACTimelock]{ + timelock, cc, tx2, deployment.NewTypeAndVersion(RBACTimelock, deployment.Version1_0_0), err2, + } + }) + if err != nil { + lggr.Errorw("Failed to deploy timelock", "err", err) + return nil, err + } + lggr.Infow("deployed timelock", "addr", timelock.Address) + // We grant the timelock the admin role on the MCMS contracts. + tx, err := timelock.Contract.GrantRole(chain.DeployerKey, + v1_0.ADMIN_ROLE.ID, timelock.Address) + if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { + lggr.Errorw("Failed to grant timelock admin role", "err", err) + return nil, err + } + // After the proposer cycle is validated, + // we can remove the deployer as an admin. + return &MCMSWithTimelockDeploy{ + Canceller: canceller, + Bypasser: bypasser, + Proposer: proposer, + Timelock: timelock, + }, nil +} diff --git a/deployment/common/changeset/internal/mcms_test.go b/deployment/common/changeset/internal/mcms_test.go new file mode 100644 index 00000000000..d0245946863 --- /dev/null +++ b/deployment/common/changeset/internal/mcms_test.go @@ -0,0 +1,54 @@ +package internal + +import ( + "encoding/json" + "testing" + + "github.com/ethereum/go-ethereum/common" + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/deployment" + common2 "github.com/smartcontractkit/chainlink/deployment/common" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func TestDeployMCMSWithConfig(t *testing.T) { + lggr := logger.TestLogger(t) + chains := memory.NewMemoryChainsWithChainIDs(t, []uint64{ + chainsel.TEST_90000001.EvmChainID, + }) + ab := deployment.NewMemoryAddressBook() + _, err := DeployMCMSWithConfig(ProposerManyChainMultisig, + lggr, chains[chainsel.TEST_90000001.Selector], ab, common2.SingleGroupMCMS(t)) + require.NoError(t, err) +} + +func TestDeployMCMSWithTimelockContracts(t *testing.T) { + lggr := logger.TestLogger(t) + chains := memory.NewMemoryChainsWithChainIDs(t, []uint64{ + chainsel.TEST_90000001.EvmChainID, + }) + ab := deployment.NewMemoryAddressBook() + _, err := DeployMCMSWithTimelockContracts(lggr, + chains[chainsel.TEST_90000001.Selector], + ab, MCMSConfig{ + Canceller: common2.SingleGroupMCMS(t), + Bypasser: common2.SingleGroupMCMS(t), + Proposer: common2.SingleGroupMCMS(t), + Executors: []common.Address{ + chains[chainsel.TEST_90000001.Selector].DeployerKey.From, + }, + }) + require.NoError(t, err) + addresses, err := ab.AddressesForChain(chainsel.TEST_90000001.Selector) + require.NoError(t, err) + require.Len(t, addresses, 4) + mcmsState, err := common2.LoadMCMSWithTimelockState(chains[chainsel.TEST_90000001.Selector], addresses) + require.NoError(t, err) + v, err := mcmsState.GenerateMCMSWithTimelockView() + b, err := json.MarshalIndent(v, "", " ") + require.NoError(t, err) + t.Log(string(b)) +} diff --git a/deployment/common/changeset/mcms_test_helpers.go b/deployment/common/changeset/mcms_test_helpers.go new file mode 100644 index 00000000000..3951149815c --- /dev/null +++ b/deployment/common/changeset/mcms_test_helpers.go @@ -0,0 +1,115 @@ +package changeset + +import ( + "bytes" + "context" + "crypto/ecdsa" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/config" + owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/deployment" +) + +var ( + // TestXXXMCMSSigner is a throwaway private key used for signing MCMS proposals. + // in tests. + TestXXXMCMSSigner *ecdsa.PrivateKey +) + +func init() { + key, err := crypto.GenerateKey() + if err != nil { + panic(err) + } + TestXXXMCMSSigner = key +} + +func SingleGroupMCMS(t *testing.T) config.Config { + publicKey := TestXXXMCMSSigner.Public().(*ecdsa.PublicKey) + // Convert the public key to an Ethereum address + address := crypto.PubkeyToAddress(*publicKey) + c, err := config.NewConfig(1, []common.Address{address}, []config.Config{}) + require.NoError(t, err) + return *c +} + +func SignProposal(t *testing.T, env deployment.Environment, proposal *timelock.MCMSWithTimelockProposal) *mcms.Executor { + executorClients := make(map[mcms.ChainIdentifier]mcms.ContractDeployBackend) + for _, chain := range env.Chains { + chainselc, exists := chainsel.ChainBySelector(chain.Selector) + require.True(t, exists) + chainSel := mcms.ChainIdentifier(chainselc.Selector) + executorClients[chainSel] = chain.Client + } + executor, err := proposal.ToExecutor(true) + require.NoError(t, err) + payload, err := executor.SigningHash() + require.NoError(t, err) + // Sign the payload + sig, err := crypto.Sign(payload.Bytes(), TestXXXMCMSSigner) + require.NoError(t, err) + mcmSig, err := mcms.NewSignatureFromBytes(sig) + require.NoError(t, err) + executor.Proposal.AddSignature(mcmSig) + require.NoError(t, executor.Proposal.Validate()) + return executor +} + +func ExecuteProposal(t *testing.T, env deployment.Environment, executor *mcms.Executor, + timelock *owner_helpers.RBACTimelock, sel uint64) { + t.Log("Executing proposal on chain", sel) + // Set the root. + tx, err2 := executor.SetRootOnChain(env.Chains[sel].Client, env.Chains[sel].DeployerKey, mcms.ChainIdentifier(sel)) + if err2 != nil { + require.NoError(t, deployment.MaybeDataErr(err2)) + } + _, err2 = env.Chains[sel].Confirm(tx) + require.NoError(t, err2) + + // TODO: This sort of helper probably should move to the MCMS lib. + // Execute all the transactions in the proposal which are for this chain. + for _, chainOp := range executor.Operations[mcms.ChainIdentifier(sel)] { + for idx, op := range executor.ChainAgnosticOps { + if bytes.Equal(op.Data, chainOp.Data) && op.To == chainOp.To { + opTx, err3 := executor.ExecuteOnChain(env.Chains[sel].Client, env.Chains[sel].DeployerKey, idx) + require.NoError(t, err3) + block, err3 := env.Chains[sel].Confirm(opTx) + require.NoError(t, err3) + t.Log("executed", chainOp) + it, err3 := timelock.FilterCallScheduled(&bind.FilterOpts{ + Start: block, + End: &block, + Context: context.Background(), + }, nil, nil) + require.NoError(t, err3) + var calls []owner_helpers.RBACTimelockCall + var pred, salt [32]byte + for it.Next() { + // Note these are the same for the whole batch, can overwrite + pred = it.Event.Predecessor + salt = it.Event.Salt + t.Log("scheduled", it.Event) + calls = append(calls, owner_helpers.RBACTimelockCall{ + Target: it.Event.Target, + Data: it.Event.Data, + Value: it.Event.Value, + }) + } + tx, err := timelock.ExecuteBatch( + env.Chains[sel].DeployerKey, calls, pred, salt) + require.NoError(t, err) + _, err = env.Chains[sel].Confirm(tx) + require.NoError(t, err) + } + } + } +} diff --git a/deployment/common/changeset/state.go b/deployment/common/changeset/state.go new file mode 100644 index 00000000000..b2dcf656aaa --- /dev/null +++ b/deployment/common/changeset/state.go @@ -0,0 +1,108 @@ +package changeset + +import ( + "errors" + + "github.com/ethereum/go-ethereum/common" + owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/view/v1_0" +) + +const ( + BypasserManyChainMultisig deployment.ContractType = "BypasserManyChainMultiSig" + CancellerManyChainMultisig deployment.ContractType = "CancellerManyChainMultiSig" + ProposerManyChainMultisig deployment.ContractType = "ProposerManyChainMultiSig" + RBACTimelock deployment.ContractType = "RBACTimelock" +) + +// MCMSWithTimelockState holds the Go bindings +// for a MCMSWithTimelock contract deployment. +// It is public for use in product specific packages. +type MCMSWithTimelockState struct { + CancellerMcm *owner_helpers.ManyChainMultiSig + BypasserMcm *owner_helpers.ManyChainMultiSig + ProposerMcm *owner_helpers.ManyChainMultiSig + Timelock *owner_helpers.RBACTimelock +} + +func (state MCMSWithTimelockState) Validate() error { + if state.Timelock == nil { + return errors.New("timelock not found") + } + if state.CancellerMcm == nil { + return errors.New("canceller not found") + } + if state.ProposerMcm == nil { + return errors.New("proposer not found") + } + if state.BypasserMcm == nil { + return errors.New("bypasser not found") + } + return nil +} + +func (state MCMSWithTimelockState) GenerateMCMSWithTimelockView() (v1_0.MCMSWithTimelockView, error) { + if err := state.Validate(); err != nil { + return v1_0.MCMSWithTimelockView{}, err + } + timelockView, err := v1_0.GenerateTimelockView(*state.Timelock) + if err != nil { + return v1_0.MCMSWithTimelockView{}, nil + } + bypasserView, err := v1_0.GenerateMCMSView(*state.BypasserMcm) + if err != nil { + return v1_0.MCMSWithTimelockView{}, nil + } + proposerView, err := v1_0.GenerateMCMSView(*state.ProposerMcm) + if err != nil { + return v1_0.MCMSWithTimelockView{}, nil + } + cancellerView, err := v1_0.GenerateMCMSView(*state.CancellerMcm) + if err != nil { + return v1_0.MCMSWithTimelockView{}, nil + } + return v1_0.MCMSWithTimelockView{ + Timelock: timelockView, + Bypasser: bypasserView, + Proposer: proposerView, + Canceller: cancellerView, + }, nil +} + +func LoadMCMSWithTimelockState(chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (*MCMSWithTimelockState, error) { + state := MCMSWithTimelockState{} + for address, tvStr := range addresses { + switch tvStr.String() { + case deployment.NewTypeAndVersion(RBACTimelock, deployment.Version1_0_0).String(): + tl, err := owner_helpers.NewRBACTimelock(common.HexToAddress(address), chain.Client) + if err != nil { + return nil, err + } + state.Timelock = tl + case deployment.NewTypeAndVersion(ProposerManyChainMultisig, deployment.Version1_0_0).String(): + mcms, err := owner_helpers.NewManyChainMultiSig(common.HexToAddress(address), chain.Client) + if err != nil { + return nil, err + } + state.ProposerMcm = mcms + case deployment.NewTypeAndVersion(BypasserManyChainMultisig, deployment.Version1_0_0).String(): + mcms, err := owner_helpers.NewManyChainMultiSig(common.HexToAddress(address), chain.Client) + if err != nil { + return nil, err + } + state.BypasserMcm = mcms + case deployment.NewTypeAndVersion(CancellerManyChainMultisig, deployment.Version1_0_0).String(): + mcms, err := owner_helpers.NewManyChainMultiSig(common.HexToAddress(address), chain.Client) + if err != nil { + return nil, err + } + state.CancellerMcm = mcms + } + } + if err := state.Validate(); err != nil { + return nil, errors.New("missing contract") + } + return &state, nil +} diff --git a/deployment/common/view/v1_0/mcms.go b/deployment/common/view/v1_0/mcms.go new file mode 100644 index 00000000000..25ca614a553 --- /dev/null +++ b/deployment/common/view/v1_0/mcms.go @@ -0,0 +1,146 @@ +package v1_0 + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/config" + owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + + "github.com/smartcontractkit/chainlink/deployment/common/view/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" +) + +type Role struct { + ID common.Hash + Name string +} + +const ( + EXECUTOR_ROLE_STR = "EXECUTOR_ROLE" + BYPASSER_ROLE_STR = "BYPASSER_ROLE" + CANCELLER_ROLE_STR = "CANCELLER_ROLE" + PROPOSER_ROLE_STR = "PROPOSER_ROLE" + ADMIN_ROLE_STR = "ADMIN_ROLE" +) + +// https://github.com/smartcontractkit/ccip-owner-contracts/blob/9d81692b324ce7ea2ef8a75e683889edbc7e2dd0/src/RBACTimelock.sol#L71 +// Just to avoid invoking the Go binding to get these. +var ( + ADMIN_ROLE = Role{ + ID: utils.MustHash(ADMIN_ROLE_STR), + Name: ADMIN_ROLE_STR, + } + PROPOSER_ROLE = Role{ + ID: utils.MustHash(PROPOSER_ROLE_STR), + Name: PROPOSER_ROLE_STR, + } + BYPASSER_ROLE = Role{ + ID: utils.MustHash(BYPASSER_ROLE_STR), + Name: BYPASSER_ROLE_STR, + } + CANCELLER_ROLE = Role{ + ID: utils.MustHash(CANCELLER_ROLE_STR), + Name: CANCELLER_ROLE_STR, + } + EXECUTOR_ROLE = Role{ + ID: utils.MustHash(EXECUTOR_ROLE_STR), + Name: EXECUTOR_ROLE_STR, + } +) + +type MCMSView struct { + types.ContractMetaData + // Note config is json marshallable. + Config config.Config `json:"config"` +} + +func GenerateMCMSView(mcms owner_helpers.ManyChainMultiSig) (MCMSView, error) { + owner, err := mcms.Owner(nil) + if err != nil { + return MCMSView{}, nil + } + c, err := mcms.GetConfig(nil) + if err != nil { + return MCMSView{}, nil + } + parsedConfig, err := config.NewConfigFromRaw(c) + if err != nil { + return MCMSView{}, nil + } + return MCMSView{ + // Has no type and version on the contract + ContractMetaData: types.ContractMetaData{ + Owner: owner, + Address: mcms.Address(), + }, + Config: *parsedConfig, + }, nil +} + +type TimelockView struct { + types.ContractMetaData + MembersByRole map[string][]common.Address `json:"membersByRole"` +} + +func GenerateTimelockView(tl owner_helpers.RBACTimelock) (TimelockView, error) { + membersByRole := make(map[string][]common.Address) + for _, role := range []Role{ADMIN_ROLE, PROPOSER_ROLE, BYPASSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE} { + numMembers, err := tl.GetRoleMemberCount(nil, role.ID) + if err != nil { + return TimelockView{}, nil + } + for i := int64(0); i < numMembers.Int64(); i++ { + member, err2 := tl.GetRoleMember(nil, role.ID, big.NewInt(i)) + if err2 != nil { + return TimelockView{}, nil + } + membersByRole[role.Name] = append(membersByRole[role.Name], member) + } + } + return TimelockView{ + // Has no type and version or owner. + ContractMetaData: types.ContractMetaData{ + Address: tl.Address(), + }, + MembersByRole: membersByRole, + }, nil +} + +type MCMSWithTimelockView struct { + Bypasser MCMSView `json:"bypasser"` + Canceller MCMSView `json:"canceller"` + Proposer MCMSView `json:"proposer"` + Timelock TimelockView `json:"timelock"` +} + +func GenerateMCMSWithTimelockView( + bypasser owner_helpers.ManyChainMultiSig, + canceller owner_helpers.ManyChainMultiSig, + proposer owner_helpers.ManyChainMultiSig, + timelock owner_helpers.RBACTimelock, +) (MCMSWithTimelockView, error) { + timelockView, err := GenerateTimelockView(timelock) + if err != nil { + return MCMSWithTimelockView{}, nil + } + bypasserView, err := GenerateMCMSView(bypasser) + if err != nil { + return MCMSWithTimelockView{}, nil + } + proposerView, err := GenerateMCMSView(proposer) + if err != nil { + return MCMSWithTimelockView{}, nil + } + cancellerView, err := GenerateMCMSView(canceller) + if err != nil { + return MCMSWithTimelockView{}, nil + } + + return MCMSWithTimelockView{ + Timelock: timelockView, + Bypasser: bypasserView, + Proposer: proposerView, + Canceller: cancellerView, + }, nil +}