diff --git a/deployment/address_book.go b/deployment/address_book.go index 7997507554f..28d728bf6c7 100644 --- a/deployment/address_book.go +++ b/deployment/address_book.go @@ -256,3 +256,18 @@ func SearchAddressBook(ab AddressBook, chain uint64, typ ContractType) (string, return "", fmt.Errorf("not found") } + +func AddressBookContains(ab AddressBook, chain uint64, addrToFind string) (bool, error) { + addrs, err := ab.AddressesForChain(chain) + if err != nil { + return false, err + } + + for addr := range addrs { + if addr == addrToFind { + return true, nil + } + } + + return false, nil +} diff --git a/deployment/ccip/changeset/accept_ownership_test.go b/deployment/ccip/changeset/accept_ownership_test.go index 7164fe1786a..796db6aed09 100644 --- a/deployment/ccip/changeset/accept_ownership_test.go +++ b/deployment/ccip/changeset/accept_ownership_test.go @@ -2,14 +2,10 @@ package changeset import ( "testing" - "time" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/deployment/environment/memory" @@ -47,15 +43,9 @@ func Test_NewAcceptOwnershipChangeset(t *testing.T) { _, err = commonchangeset.ApplyChangesets(t, e.Env, timelocks, []commonchangeset.ChangesetApplication{ // note this doesn't have proposals. { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.NewTransferOwnershipChangeset), + Changeset: commonchangeset.WrapChangeSet(commonchangeset.TransferToMCMSWithTimelock), Config: genTestTransferOwnershipConfig(e, allChains, state), }, - // this has proposals, ApplyChangesets will sign & execute them. - // in practice, signing and executing are separated processes. - { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.NewAcceptOwnershipChangeset), - Config: genTestAcceptOwnershipConfig(e, allChains, state), - }, }) require.NoError(t, err) @@ -66,21 +56,21 @@ func genTestTransferOwnershipConfig( e DeployedEnv, chains []uint64, state CCIPOnChainState, -) commonchangeset.TransferOwnershipConfig { +) commonchangeset.TransferToMCMSWithTimelockConfig { var ( timelocksPerChain = make(map[uint64]common.Address) - contracts = make(map[uint64][]commonchangeset.OwnershipTransferrer) + contracts = make(map[uint64][]common.Address) ) // chain contracts for _, chain := range chains { timelocksPerChain[chain] = state.Chains[chain].Timelock.Address() - contracts[chain] = []commonchangeset.OwnershipTransferrer{ - state.Chains[chain].OnRamp, - state.Chains[chain].OffRamp, - state.Chains[chain].FeeQuoter, - state.Chains[chain].NonceManager, - state.Chains[chain].RMNRemote, + contracts[chain] = []common.Address{ + state.Chains[chain].OnRamp.Address(), + state.Chains[chain].OffRamp.Address(), + state.Chains[chain].FeeQuoter.Address(), + state.Chains[chain].NonceManager.Address(), + state.Chains[chain].RMNRemote.Address(), } } @@ -88,54 +78,13 @@ func genTestTransferOwnershipConfig( homeChainTimelockAddress := state.Chains[e.HomeChainSel].Timelock.Address() timelocksPerChain[e.HomeChainSel] = homeChainTimelockAddress contracts[e.HomeChainSel] = append(contracts[e.HomeChainSel], - state.Chains[e.HomeChainSel].CapabilityRegistry, - state.Chains[e.HomeChainSel].CCIPHome, - state.Chains[e.HomeChainSel].RMNHome, - ) - - return commonchangeset.TransferOwnershipConfig{ - OwnersPerChain: timelocksPerChain, - Contracts: contracts, - } -} - -func genTestAcceptOwnershipConfig( - e DeployedEnv, - chains []uint64, - state CCIPOnChainState, -) commonchangeset.AcceptOwnershipConfig { - var ( - timelocksPerChain = make(map[uint64]common.Address) - proposerMCMses = make(map[uint64]*gethwrappers.ManyChainMultiSig) - contracts = make(map[uint64][]commonchangeset.OwnershipAcceptor) - ) - for _, chain := range chains { - timelocksPerChain[chain] = state.Chains[chain].Timelock.Address() - proposerMCMses[chain] = state.Chains[chain].ProposerMcm - contracts[chain] = []commonchangeset.OwnershipAcceptor{ - state.Chains[chain].OnRamp, - state.Chains[chain].OffRamp, - state.Chains[chain].FeeQuoter, - state.Chains[chain].NonceManager, - state.Chains[chain].RMNRemote, - } - } - - // add home chain contracts. - // this overwrite should be fine. - timelocksPerChain[e.HomeChainSel] = state.Chains[e.HomeChainSel].Timelock.Address() - proposerMCMses[e.HomeChainSel] = state.Chains[e.HomeChainSel].ProposerMcm - contracts[e.HomeChainSel] = append(contracts[e.HomeChainSel], - state.Chains[e.HomeChainSel].CapabilityRegistry, - state.Chains[e.HomeChainSel].CCIPHome, - state.Chains[e.HomeChainSel].RMNHome, + state.Chains[e.HomeChainSel].CapabilityRegistry.Address(), + state.Chains[e.HomeChainSel].CCIPHome.Address(), + state.Chains[e.HomeChainSel].RMNHome.Address(), ) - return commonchangeset.AcceptOwnershipConfig{ - OwnersPerChain: timelocksPerChain, - ProposerMCMSes: proposerMCMses, - Contracts: contracts, - MinDelay: time.Duration(0), + return commonchangeset.TransferToMCMSWithTimelockConfig{ + ContractsByChain: contracts, } } @@ -147,19 +96,16 @@ func assertTimelockOwnership( chains []uint64, state CCIPOnChainState, ) { - ctx := tests.Context(t) // check that the ownership has been transferred correctly for _, chain := range chains { - for _, contract := range []commonchangeset.OwnershipTransferrer{ - state.Chains[chain].OnRamp, - state.Chains[chain].OffRamp, - state.Chains[chain].FeeQuoter, - state.Chains[chain].NonceManager, - state.Chains[chain].RMNRemote, + for _, contract := range []common.Address{ + state.Chains[chain].OnRamp.Address(), + state.Chains[chain].OffRamp.Address(), + state.Chains[chain].FeeQuoter.Address(), + state.Chains[chain].NonceManager.Address(), + state.Chains[chain].RMNRemote.Address(), } { - owner, err := contract.Owner(&bind.CallOpts{ - Context: ctx, - }) + owner, _, err := commonchangeset.LoadOwnableContract(contract, e.Env.Chains[chain].Client) require.NoError(t, err) require.Equal(t, state.Chains[chain].Timelock.Address(), owner) } @@ -167,14 +113,12 @@ func assertTimelockOwnership( // check home chain contracts ownership homeChainTimelockAddress := state.Chains[e.HomeChainSel].Timelock.Address() - for _, contract := range []commonchangeset.OwnershipTransferrer{ - state.Chains[e.HomeChainSel].CapabilityRegistry, - state.Chains[e.HomeChainSel].CCIPHome, - state.Chains[e.HomeChainSel].RMNHome, + for _, contract := range []common.Address{ + state.Chains[e.HomeChainSel].CapabilityRegistry.Address(), + state.Chains[e.HomeChainSel].CCIPHome.Address(), + state.Chains[e.HomeChainSel].RMNHome.Address(), } { - owner, err := contract.Owner(&bind.CallOpts{ - Context: ctx, - }) + owner, _, err := commonchangeset.LoadOwnableContract(contract, e.Env.Chains[e.HomeChainSel].Client) require.NoError(t, err) require.Equal(t, homeChainTimelockAddress, owner) } diff --git a/deployment/ccip/changeset/cs_active_candidate_test.go b/deployment/ccip/changeset/cs_active_candidate_test.go index 6efdacc3b7c..4bd0c9fd7a4 100644 --- a/deployment/ccip/changeset/cs_active_candidate_test.go +++ b/deployment/ccip/changeset/cs_active_candidate_test.go @@ -98,15 +98,9 @@ func TestActiveCandidate(t *testing.T) { _, err = commonchangeset.ApplyChangesets(t, e, timelocks, []commonchangeset.ChangesetApplication{ // note this doesn't have proposals. { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.NewTransferOwnershipChangeset), + Changeset: commonchangeset.WrapChangeSet(commonchangeset.TransferToMCMSWithTimelock), Config: genTestTransferOwnershipConfig(tenv, allChains, state), }, - // this has proposals, ApplyChangesets will sign & execute them. - // in practice, signing and executing are separated processes. - { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.NewAcceptOwnershipChangeset), - Config: genTestAcceptOwnershipConfig(tenv, allChains, state), - }, }) require.NoError(t, err) // Apply the accept ownership proposal to all the chains. diff --git a/deployment/ccip/changeset/cs_add_chain_test.go b/deployment/ccip/changeset/cs_add_chain_test.go index e53a147edea..b8a845ac27c 100644 --- a/deployment/ccip/changeset/cs_add_chain_test.go +++ b/deployment/ccip/changeset/cs_add_chain_test.go @@ -58,13 +58,21 @@ func TestAddChainInbound(t *testing.T) { TimelockExecutors: e.Env.AllDeployerKeys(), TimelockMinDelay: big.NewInt(0), } - out, err := commonchangeset.DeployMCMSWithTimelock(e.Env, map[uint64]commontypes.MCMSWithTimelockConfig{ - initialDeploy[0]: cfg, - initialDeploy[1]: cfg, - initialDeploy[2]: cfg, + e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, nil, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployLinkToken), + Config: initialDeploy, + }, + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployMCMSWithTimelock), + Config: map[uint64]commontypes.MCMSWithTimelockConfig{ + initialDeploy[0]: cfg, + initialDeploy[1]: cfg, + initialDeploy[2]: cfg, + }, + }, }) require.NoError(t, err) - require.NoError(t, e.Env.ExistingAddresses.Merge(out.AddressBook)) newAddresses = deployment.NewMemoryAddressBook() tokenConfig := NewTestTokenConfig(state.Chains[e.FeedChainSel].USDFeeds) @@ -99,12 +107,19 @@ func TestAddChainInbound(t *testing.T) { require.NoError(t, err) // Deploy contracts to new chain - out, err = commonchangeset.DeployMCMSWithTimelock(e.Env, map[uint64]commontypes.MCMSWithTimelockConfig{ - newChain: cfg, + e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, nil, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployLinkToken), + Config: []uint64{newChain}, + }, + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployMCMSWithTimelock), + Config: map[uint64]commontypes.MCMSWithTimelockConfig{ + newChain: cfg, + }, + }, }) require.NoError(t, err) - require.NoError(t, e.Env.ExistingAddresses.Merge(out.AddressBook)) - newAddresses = deployment.NewMemoryAddressBook() err = deployPrerequisiteChainContracts(e.Env, newAddresses, []uint64{newChain}, nil) @@ -138,15 +153,9 @@ func TestAddChainInbound(t *testing.T) { }, []commonchangeset.ChangesetApplication{ // note this doesn't have proposals. { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.NewTransferOwnershipChangeset), + Changeset: commonchangeset.WrapChangeSet(commonchangeset.TransferToMCMSWithTimelock), Config: genTestTransferOwnershipConfig(e, initialDeploy, state), }, - // this has proposals, ApplyChangesets will sign & execute them. - // in practice, signing and executing are separated processes. - { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.NewAcceptOwnershipChangeset), - Config: genTestAcceptOwnershipConfig(e, initialDeploy, state), - }, }) require.NoError(t, err) diff --git a/deployment/ccip/changeset/cs_deploy_chain_test.go b/deployment/ccip/changeset/cs_deploy_chain_test.go index 7965fe8e725..234d73cc4b5 100644 --- a/deployment/ccip/changeset/cs_deploy_chain_test.go +++ b/deployment/ccip/changeset/cs_deploy_chain_test.go @@ -28,27 +28,6 @@ func TestDeployChainContractsChangeset(t *testing.T) { nodes, err := deployment.NodeInfo(e.NodeIDs, e.Offchain) require.NoError(t, err) p2pIds := nodes.NonBootstraps().PeerIDs() - // deploy home chain - homeChainCfg := DeployHomeChainConfig{ - HomeChainSel: homeChainSel, - RMNStaticConfig: NewTestRMNStaticConfig(), - RMNDynamicConfig: NewTestRMNDynamicConfig(), - NodeOperators: NewTestNodeOperator(e.Chains[homeChainSel].DeployerKey.From), - NodeP2PIDsPerNodeOpAdmin: map[string][][32]byte{ - "NodeOperator": p2pIds, - }, - } - output, err := DeployHomeChain(e, homeChainCfg) - require.NoError(t, err) - require.NoError(t, e.ExistingAddresses.Merge(output.AddressBook)) - - // deploy pre-requisites - prerequisites, err := DeployPrerequisites(e, DeployPrerequisiteConfig{ - ChainSelectors: selectors, - }) - require.NoError(t, err) - require.NoError(t, e.ExistingAddresses.Merge(prerequisites.AddressBook)) - cfg := make(map[uint64]commontypes.MCMSWithTimelockConfig) for _, chain := range e.AllChainSelectors() { cfg[chain] = commontypes.MCMSWithTimelockConfig{ @@ -59,17 +38,42 @@ func TestDeployChainContractsChangeset(t *testing.T) { TimelockMinDelay: big.NewInt(0), } } - output, err = commonchangeset.DeployMCMSWithTimelock(e, cfg) - require.NoError(t, err) - require.NoError(t, e.ExistingAddresses.Merge(output.AddressBook)) - - // deploy ccip chain contracts - output, err = DeployChainContracts(e, DeployChainContractsConfig{ - ChainSelectors: selectors, - HomeChainSelector: homeChainSel, + e, err = commonchangeset.ApplyChangesets(t, e, nil, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(DeployHomeChain), + Config: DeployHomeChainConfig{ + HomeChainSel: homeChainSel, + RMNStaticConfig: NewTestRMNStaticConfig(), + RMNDynamicConfig: NewTestRMNDynamicConfig(), + NodeOperators: NewTestNodeOperator(e.Chains[homeChainSel].DeployerKey.From), + NodeP2PIDsPerNodeOpAdmin: map[string][][32]byte{ + "NodeOperator": p2pIds, + }, + }, + }, + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployLinkToken), + Config: selectors, + }, + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployMCMSWithTimelock), + Config: cfg, + }, + { + Changeset: commonchangeset.WrapChangeSet(DeployPrerequisites), + Config: DeployPrerequisiteConfig{ + ChainSelectors: selectors, + }, + }, + { + Changeset: commonchangeset.WrapChangeSet(DeployChainContracts), + Config: DeployChainContractsConfig{ + ChainSelectors: selectors, + HomeChainSelector: homeChainSel, + }, + }, }) require.NoError(t, err) - require.NoError(t, e.ExistingAddresses.Merge(output.AddressBook)) // load onchain state state, err := LoadOnchainState(e) diff --git a/deployment/ccip/changeset/cs_prerequisites.go b/deployment/ccip/changeset/cs_prerequisites.go index f6c502d9ad5..e610dfaaeeb 100644 --- a/deployment/ccip/changeset/cs_prerequisites.go +++ b/deployment/ccip/changeset/cs_prerequisites.go @@ -2,7 +2,6 @@ package changeset import ( "fmt" - "math/big" "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" @@ -16,7 +15,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_admin_registry" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/weth9" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/burn_mint_erc677" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/multicall3" ) @@ -126,7 +124,6 @@ func deployPrerequisiteContracts(e deployment.Environment, ab deployment.Address lggr := e.Logger chainState, chainExists := state.Chains[chain.Selector] var weth9Contract *weth9.WETH9 - var linkTokenContract *burn_mint_erc677.BurnMintERC677 var tokenAdminReg *token_admin_registry.TokenAdminRegistry var registryModule *registry_module_owner_custom.RegistryModuleOwnerCustom var rmnProxy *rmn_proxy_contract.RMNProxyContract @@ -134,7 +131,6 @@ func deployPrerequisiteContracts(e deployment.Environment, ab deployment.Address var mc3 *multicall3.Multicall3 if chainExists { weth9Contract = chainState.Weth9 - linkTokenContract = chainState.LinkToken tokenAdminReg = chainState.TokenAdminRegistry registryModule = chainState.RegistryModule rmnProxy = chainState.RMNProxyExisting @@ -257,29 +253,6 @@ func deployPrerequisiteContracts(e deployment.Environment, ab deployment.Address } else { lggr.Infow("weth9 already deployed", "addr", weth9Contract.Address) } - if linkTokenContract == nil { - linkToken, err := deployment.DeployContract(lggr, chain, ab, - func(chain deployment.Chain) deployment.ContractDeploy[*burn_mint_erc677.BurnMintERC677] { - linkTokenAddr, tx2, linkToken, err2 := burn_mint_erc677.DeployBurnMintERC677( - chain.DeployerKey, - chain.Client, - "Link Token", - "LINK", - uint8(18), - big.NewInt(0).Mul(big.NewInt(1e9), big.NewInt(1e18)), - ) - return deployment.ContractDeploy[*burn_mint_erc677.BurnMintERC677]{ - linkTokenAddr, linkToken, tx2, deployment.NewTypeAndVersion(LinkToken, deployment.Version1_0_0), err2, - } - }) - if err != nil { - lggr.Errorw("Failed to deploy linkToken", "err", err) - return err - } - lggr.Infow("deployed linkToken", "addr", linkToken.Address) - } else { - lggr.Infow("linkToken already deployed", "addr", linkTokenContract.Address) - } // if router is not already deployed, we deploy it if r == nil { routerContract, err := deployment.DeployContract(e.Logger, chain, ab, diff --git a/deployment/ccip/changeset/cs_prerequisites_test.go b/deployment/ccip/changeset/cs_prerequisites_test.go index 1a167b2816c..da1ff9c83a9 100644 --- a/deployment/ccip/changeset/cs_prerequisites_test.go +++ b/deployment/ccip/changeset/cs_prerequisites_test.go @@ -28,7 +28,6 @@ func TestDeployPrerequisites(t *testing.T) { require.NoError(t, err) state, err := LoadOnchainState(e) require.NoError(t, err) - require.NotNil(t, state.Chains[newChain].LinkToken) require.NotNil(t, state.Chains[newChain].Weth9) require.NotNil(t, state.Chains[newChain].TokenAdminRegistry) require.NotNil(t, state.Chains[newChain].RegistryModule) diff --git a/deployment/ccip/changeset/save_existing_test.go b/deployment/ccip/changeset/save_existing_test.go index 93f3d7e067d..080ed80481a 100644 --- a/deployment/ccip/changeset/save_existing_test.go +++ b/deployment/ccip/changeset/save_existing_test.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/chainlink/deployment" commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/deployment/environment/memory" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -29,7 +30,7 @@ func TestSaveExistingCCIP(t *testing.T) { ExistingContracts: []commonchangeset.Contract{ { Address: common.BigToAddress(big.NewInt(1)), - TypeAndVersion: deployment.NewTypeAndVersion(LinkToken, deployment.Version1_0_0), + TypeAndVersion: deployment.NewTypeAndVersion(commontypes.LinkToken, deployment.Version1_0_0), ChainSelector: chain1, }, { diff --git a/deployment/ccip/changeset/state.go b/deployment/ccip/changeset/state.go index fe7b7008982..20763523141 100644 --- a/deployment/ccip/changeset/state.go +++ b/deployment/ccip/changeset/state.go @@ -51,7 +51,6 @@ import ( var ( MockRMN deployment.ContractType = "MockRMN" RMNRemote deployment.ContractType = "RMNRemote" - LinkToken deployment.ContractType = "LinkToken" ARMProxy deployment.ContractType = "ARMProxy" WETH9 deployment.ContractType = "WETH9" Router deployment.ContractType = "Router" @@ -83,6 +82,7 @@ var ( // on a chain. If a binding is nil, it means here is no such contract on the chain. type CCIPChainState struct { commoncs.MCMSWithTimelockState + commoncs.LinkTokenState OnRamp *onramp.OnRamp OffRamp *offramp.OffRamp FeeQuoter *fee_quoter.FeeQuoter @@ -103,8 +103,6 @@ type CCIPChainState struct { Weth9 *weth9.WETH9 RMNRemote *rmn_remote.RMNRemote MockRMN *mock_rmn_contract.MockRMNContract - // TODO: May need to support older link too - LinkToken *burn_mint_erc677.BurnMintERC677 // Map between token Descriptor (e.g. LinkSymbol, WethSymbol) // and the respective token contract // This is more of an illustration of how we'll have tokens, and it might need some work later to work properly. @@ -221,6 +219,13 @@ func (c CCIPChainState) GenerateView() (view.ChainView, error) { } chainView.MCMSWithTimelock = mcmsView } + if c.LinkToken != nil { + linkTokenView, err := common_v1_0.GenerateLinkTokenView(c.LinkToken) + if err != nil { + return chainView, err + } + chainView.LinkToken = linkTokenView + } return chainView, nil } @@ -290,12 +295,19 @@ func LoadChainState(chain deployment.Chain, addresses map[string]deployment.Type return state, err } state.MCMSWithTimelockState = *mcmsWithTimelock + + linkState, err := commoncs.LoadLinkTokenState(chain, addresses) + if err != nil { + return state, err + } + state.LinkTokenState = *linkState for address, tvStr := range addresses { switch tvStr.String() { case deployment.NewTypeAndVersion(commontypes.RBACTimelock, deployment.Version1_0_0).String(), deployment.NewTypeAndVersion(commontypes.ProposerManyChainMultisig, deployment.Version1_0_0).String(), deployment.NewTypeAndVersion(commontypes.CancellerManyChainMultisig, deployment.Version1_0_0).String(), - deployment.NewTypeAndVersion(commontypes.BypasserManyChainMultisig, deployment.Version1_0_0).String(): + deployment.NewTypeAndVersion(commontypes.BypasserManyChainMultisig, deployment.Version1_0_0).String(), + deployment.NewTypeAndVersion(commontypes.LinkToken, deployment.Version1_0_0).String(): continue case deployment.NewTypeAndVersion(CapabilitiesRegistry, deployment.Version1_0_0).String(): cr, err := capabilities_registry.NewCapabilitiesRegistry(common.HexToAddress(address), chain.Client) @@ -399,12 +411,6 @@ func LoadChainState(chain deployment.Chain, addresses map[string]deployment.Type return state, err } state.FeeQuoter = fq - case deployment.NewTypeAndVersion(LinkToken, deployment.Version1_0_0).String(): - lt, err := burn_mint_erc677.NewBurnMintERC677(common.HexToAddress(address), chain.Client) - if err != nil { - return state, err - } - state.LinkToken = lt case deployment.NewTypeAndVersion(USDCToken, deployment.Version1_0_0).String(): ut, err := burn_mint_erc677.NewBurnMintERC677(common.HexToAddress(address), chain.Client) if err != nil { diff --git a/deployment/ccip/changeset/test_helpers.go b/deployment/ccip/changeset/test_helpers.go index a5a5881a4e5..742fe39200a 100644 --- a/deployment/ccip/changeset/test_helpers.go +++ b/deployment/ccip/changeset/test_helpers.go @@ -293,6 +293,10 @@ func NewMemoryEnvironmentWithJobsAndContracts(t *testing.T, lggr logger.Logger, // Need to deploy prerequisites first so that we can form the USDC config // no proposals to be made, timelock can be passed as nil here e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, nil, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployLinkToken), + Config: allChains, + }, { Changeset: commonchangeset.WrapChangeSet(DeployPrerequisites), Config: DeployPrerequisiteConfig{ diff --git a/deployment/ccip/changeset/token_info.go b/deployment/ccip/changeset/token_info.go index 5bd6b2ed66e..7c008a8a884 100644 --- a/deployment/ccip/changeset/token_info.go +++ b/deployment/ccip/changeset/token_info.go @@ -3,12 +3,11 @@ package changeset import ( "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink-ccip/pluginconfig" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/link_token" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/weth9" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/aggregator_v3_interface" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/burn_mint_erc677" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" ) type TokenSymbol string @@ -68,7 +67,7 @@ func (tc *TokenConfig) UpsertTokenInfo( // GetTokenInfo Adds mapping between dest chain tokens and their respective aggregators on feed chain. func (tc *TokenConfig) GetTokenInfo( lggr logger.Logger, - linkToken *burn_mint_erc677.BurnMintERC677, + linkToken *link_token.LinkToken, wethToken *weth9.WETH9, ) map[ccipocr3.UnknownEncodedAddress]pluginconfig.TokenInfo { tokenToAggregate := make(map[ccipocr3.UnknownEncodedAddress]pluginconfig.TokenInfo) diff --git a/deployment/ccip/view/view.go b/deployment/ccip/view/view.go index 318e09100b9..836dc9c65dd 100644 --- a/deployment/ccip/view/view.go +++ b/deployment/ccip/view/view.go @@ -27,6 +27,7 @@ type ChainView struct { 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"` + LinkToken common_v1_0.LinkTokenView `json:"linkToken,omitempty"` } func NewChain() ChainView { diff --git a/deployment/common/changeset/accept_ownership.go b/deployment/common/changeset/accept_ownership.go deleted file mode 100644 index 79aa876eabb..00000000000 --- a/deployment/common/changeset/accept_ownership.go +++ /dev/null @@ -1,105 +0,0 @@ -package changeset - -import ( - "fmt" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - gethtypes "github.com/ethereum/go-ethereum/core/types" - - "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" - - "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" -) - -type OwnershipAcceptor interface { - AcceptOwnership(opts *bind.TransactOpts) (*gethtypes.Transaction, error) - Address() common.Address -} - -type AcceptOwnershipConfig struct { - // OwnersPerChain is a mapping from chain selector to the owner contract address on that chain. - OwnersPerChain map[uint64]common.Address - - // ProposerMCMSes is a mapping from chain selector to the proposer MCMS contract on that chain. - ProposerMCMSes map[uint64]*gethwrappers.ManyChainMultiSig - - // Contracts is a mapping from chain selector to the ownership acceptors on that chain. - // Proposal will be generated for these contracts. - Contracts map[uint64][]OwnershipAcceptor - - // MinDelay is the minimum amount of time that must pass before the proposal - // can be executed onchain. - // This is typically set to 3 hours but can be set to 0 for immediate execution (useful for tests). - MinDelay time.Duration -} - -func (a AcceptOwnershipConfig) Validate() error { - // check that we have owners and proposer mcmses for the chains - // in the Contracts field. - for chainSelector := range a.Contracts { - if _, ok := a.OwnersPerChain[chainSelector]; !ok { - return fmt.Errorf("missing owner for chain %d", chainSelector) - } - if _, ok := a.ProposerMCMSes[chainSelector]; !ok { - return fmt.Errorf("missing proposer MCMS for chain %d", chainSelector) - } - } - - return nil -} - -// type assertion - comply with deployment.ChangeSet interface -var _ deployment.ChangeSet[AcceptOwnershipConfig] = NewAcceptOwnershipChangeset - -// NewAcceptOwnershipChangeset creates a changeset that contains a proposal to accept ownership of the contracts -// provided in the configuration. -func NewAcceptOwnershipChangeset( - e deployment.Environment, - cfg AcceptOwnershipConfig, -) (deployment.ChangesetOutput, error) { - if err := cfg.Validate(); err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("invalid accept ownership config: %w", err) - } - - var batches []timelock.BatchChainOperation - for chainSelector, ownershipAcceptors := range cfg.Contracts { - var ops []mcms.Operation - for _, ownershipAcceptor := range ownershipAcceptors { - tx, err := ownershipAcceptor.AcceptOwnership(deployment.SimTransactOpts()) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate accept ownership calldata of %T: %w", ownershipAcceptor, err) - } - - ops = append(ops, mcms.Operation{ - To: ownershipAcceptor.Address(), - Data: tx.Data(), - Value: big.NewInt(0), - }) - } - batches = append(batches, timelock.BatchChainOperation{ - ChainIdentifier: mcms.ChainIdentifier(chainSelector), - Batch: ops, - }) - } - - proposal, err := proposalutils.BuildProposalFromBatches( - cfg.OwnersPerChain, - cfg.ProposerMCMSes, - batches, - "Accept ownership of contracts", - cfg.MinDelay, - ) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal from batch: %w, batches: %+v", err, batches) - } - - return deployment.ChangesetOutput{ - Proposals: []timelock.MCMSWithTimelockProposal{*proposal}, - }, nil -} diff --git a/deployment/common/changeset/accept_ownership_test.go b/deployment/common/changeset/accept_ownership_test.go deleted file mode 100644 index fc2c296f2d8..00000000000 --- a/deployment/common/changeset/accept_ownership_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package changeset_test - -import ( - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" - "github.com/stretchr/testify/assert" - - "github.com/smartcontractkit/chainlink/deployment/common/changeset" -) - -func TestAcceptOwnershipConfig_Validate(t *testing.T) { - tests := []struct { - name string - config changeset.AcceptOwnershipConfig - wantErr bool - }{ - { - name: "valid config", - config: changeset.AcceptOwnershipConfig{ - OwnersPerChain: map[uint64]common.Address{ - 1: common.HexToAddress("0x1"), - }, - ProposerMCMSes: map[uint64]*gethwrappers.ManyChainMultiSig{ - 1: {}, - }, - Contracts: map[uint64][]changeset.OwnershipAcceptor{ - 1: {}, - }, - MinDelay: 3 * time.Hour, - }, - wantErr: false, - }, - { - name: "missing timelock", - config: changeset.AcceptOwnershipConfig{ - OwnersPerChain: map[uint64]common.Address{}, - ProposerMCMSes: map[uint64]*gethwrappers.ManyChainMultiSig{ - 1: {}, - }, - Contracts: map[uint64][]changeset.OwnershipAcceptor{ - 1: {}, - }, - MinDelay: 3 * time.Hour, - }, - wantErr: true, - }, - { - name: "missing proposer MCMS", - config: changeset.AcceptOwnershipConfig{ - OwnersPerChain: map[uint64]common.Address{ - 1: common.HexToAddress("0x1"), - }, - ProposerMCMSes: map[uint64]*gethwrappers.ManyChainMultiSig{}, - Contracts: map[uint64][]changeset.OwnershipAcceptor{ - 1: {}, - }, - MinDelay: 3 * time.Hour, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.config.Validate() - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} diff --git a/deployment/common/changeset/deploy_link_token.go b/deployment/common/changeset/deploy_link_token.go index 5f88b410f67..5728e977c47 100644 --- a/deployment/common/changeset/deploy_link_token.go +++ b/deployment/common/changeset/deploy_link_token.go @@ -10,20 +10,24 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/link_token" ) -var _ deployment.ChangeSet[uint64] = DeployLinkToken +var _ deployment.ChangeSet[[]uint64] = DeployLinkToken // DeployLinkToken deploys a link token contract to the chain identified by the chainSelector. -func DeployLinkToken(e deployment.Environment, chainSelector uint64) (deployment.ChangesetOutput, error) { - c, ok := e.Chains[chainSelector] - if !ok { - return deployment.ChangesetOutput{}, fmt.Errorf("chain not found in environment") +func DeployLinkToken(e deployment.Environment, chains []uint64) (deployment.ChangesetOutput, error) { + for _, chain := range chains { + _, ok := e.Chains[chain] + if !ok { + return deployment.ChangesetOutput{}, fmt.Errorf("chain not found in environment") + } } newAddresses := deployment.NewMemoryAddressBook() - _, err := deployLinkTokenContract( - e.Logger, c, newAddresses, - ) - if err != nil { - return deployment.ChangesetOutput{AddressBook: newAddresses}, err + for _, chain := range chains { + _, err := deployLinkTokenContract( + e.Logger, e.Chains[chain], newAddresses, + ) + if err != nil { + return deployment.ChangesetOutput{AddressBook: newAddresses}, err + } } return deployment.ChangesetOutput{AddressBook: newAddresses}, nil } diff --git a/deployment/common/changeset/deploy_link_token_test.go b/deployment/common/changeset/deploy_link_token_test.go index a18e0181623..29a9ece1478 100644 --- a/deployment/common/changeset/deploy_link_token_test.go +++ b/deployment/common/changeset/deploy_link_token_test.go @@ -3,37 +3,37 @@ package changeset_test import ( "testing" - "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/deployment/environment/memory" + "github.com/smartcontractkit/chainlink/v2/core/logger" ) func TestDeployLinkToken(t *testing.T) { t.Parallel() - - lggr := logger.Test(t) - cfg := memory.MemoryEnvironmentConfig{ - Nodes: 1, - Chains: 2, - } - env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) - chainSelector := env.AllChainSelectors()[0] - - resp, err := changeset.DeployLinkToken(env, chainSelector) + lggr := logger.TestLogger(t) + e := memory.NewMemoryEnvironment(t, lggr, zapcore.InfoLevel, memory.MemoryEnvironmentConfig{ + Chains: 1, + }) + chain1 := e.AllChainSelectors()[0] + e, err := changeset.ApplyChangesets(t, e, nil, []changeset.ChangesetApplication{ + { + Changeset: changeset.WrapChangeSet(changeset.DeployLinkToken), + Config: []uint64{chain1}, + }, + }) require.NoError(t, err) - require.NotNil(t, resp) - - // LinkToken should be deployed on chain 0 - addrs, err := resp.AddressBook.AddressesForChain(chainSelector) + addrs, err := e.ExistingAddresses.AddressesForChain(chain1) require.NoError(t, err) - require.Len(t, addrs, 1) - - // nothing on chain 1 - require.NotEqual(t, chainSelector, env.AllChainSelectors()[1]) - oaddrs, _ := resp.AddressBook.AddressesForChain(env.AllChainSelectors()[1]) - assert.Len(t, oaddrs, 0) + state, err := changeset.LoadLinkTokenState(e.Chains[chain1], addrs) + require.NoError(t, err) + view, err := state.GenerateLinkView() + require.NoError(t, err) + assert.Equal(t, view.Owner, e.Chains[chain1].DeployerKey.From) + assert.Equal(t, view.TypeAndVersion, "LinkToken 1.0.0") + // Initially nothing minted. + assert.Equal(t, view.Supply.String(), "0") } diff --git a/deployment/common/changeset/state.go b/deployment/common/changeset/state.go index 38a1d02c044..f553e124a38 100644 --- a/deployment/common/changeset/state.go +++ b/deployment/common/changeset/state.go @@ -9,6 +9,7 @@ import ( "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/deployment/common/view/v1_0" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/link_token" ) // MCMSWithTimelockState holds the Go bindings @@ -97,3 +98,28 @@ func LoadMCMSWithTimelockState(chain deployment.Chain, addresses map[string]depl } return &state, nil } + +type LinkTokenState struct { + LinkToken *link_token.LinkToken +} + +func (s LinkTokenState) GenerateLinkView() (v1_0.LinkTokenView, error) { + if s.LinkToken == nil { + return v1_0.LinkTokenView{}, errors.New("link token not found") + } + return v1_0.GenerateLinkTokenView(s.LinkToken) +} + +func LoadLinkTokenState(chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (*LinkTokenState, error) { + state := LinkTokenState{} + for address, tvStr := range addresses { + if tvStr.String() == deployment.NewTypeAndVersion(types.LinkToken, deployment.Version1_0_0).String() { + lt, err := link_token.NewLinkToken(common.HexToAddress(address), chain.Client) + if err != nil { + return nil, err + } + state.LinkToken = lt + } + } + return &state, nil +} diff --git a/deployment/common/changeset/transfer_ownership.go b/deployment/common/changeset/transfer_ownership.go deleted file mode 100644 index 36fe2cbed78..00000000000 --- a/deployment/common/changeset/transfer_ownership.go +++ /dev/null @@ -1,71 +0,0 @@ -package changeset - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - gethtypes "github.com/ethereum/go-ethereum/core/types" - - "github.com/smartcontractkit/chainlink/deployment" -) - -type OwnershipTransferrer interface { - TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*gethtypes.Transaction, error) - Owner(opts *bind.CallOpts) (common.Address, error) -} - -type TransferOwnershipConfig struct { - // OwnersPerChain is a mapping from chain selector to the owner's contract address on that chain. - OwnersPerChain map[uint64]common.Address - - // Contracts is a mapping from chain selector to the ownership transferrers on that chain. - Contracts map[uint64][]OwnershipTransferrer -} - -func (t TransferOwnershipConfig) Validate() error { - // check that we have owners for the chains in the Contracts field. - for chainSelector := range t.Contracts { - if _, ok := t.OwnersPerChain[chainSelector]; !ok { - return fmt.Errorf("missing owners for chain %d", chainSelector) - } - } - - return nil -} - -var _ deployment.ChangeSet[TransferOwnershipConfig] = NewTransferOwnershipChangeset - -// NewTransferOwnershipChangeset creates a changeset that transfers ownership of all the -// contracts in the provided configuration to correct owner on that chain. -// If the owner is already the provided address, no transaction is sent. -func NewTransferOwnershipChangeset( - e deployment.Environment, - cfg TransferOwnershipConfig, -) (deployment.ChangesetOutput, error) { - if err := cfg.Validate(); err != nil { - return deployment.ChangesetOutput{}, err - } - - for chainSelector, contracts := range cfg.Contracts { - ownerAddress := cfg.OwnersPerChain[chainSelector] - for _, contract := range contracts { - owner, err := contract.Owner(nil) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to get owner of contract %T: %v", contract, err) - } - if owner != ownerAddress { - tx, err := contract.TransferOwnership(e.Chains[chainSelector].DeployerKey, ownerAddress) - _, err = deployment.ConfirmIfNoError(e.Chains[chainSelector], tx, err) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to transfer ownership of contract %T: %v", contract, err) - } - } - } - } - - // no new addresses or proposals or jobspecs, so changeset output is empty. - // NOTE: onchain state has technically changed for above contracts, maybe that should - // be captured? - return deployment.ChangesetOutput{}, nil -} diff --git a/deployment/common/changeset/transfer_to_mcms_with_timelock.go b/deployment/common/changeset/transfer_to_mcms_with_timelock.go new file mode 100644 index 00000000000..e48d29af92b --- /dev/null +++ b/deployment/common/changeset/transfer_to_mcms_with_timelock.go @@ -0,0 +1,144 @@ +package changeset + +import ( + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + 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" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/burn_mint_erc677" +) + +type TransferToMCMSWithTimelockConfig struct { + ContractsByChain map[uint64][]common.Address + // MinDelay is for the accept ownership proposal + MinDelay time.Duration +} + +type Ownable interface { + Owner(opts *bind.CallOpts) (common.Address, error) + TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*gethtypes.Transaction, error) + AcceptOwnership(opts *bind.TransactOpts) (*gethtypes.Transaction, error) + Address() common.Address +} + +func LoadOwnableContract(addr common.Address, client bind.ContractBackend) (common.Address, Ownable, error) { + // Just using the ownership interface from here. + c, err := burn_mint_erc677.NewBurnMintERC677(addr, client) + if err != nil { + return common.Address{}, nil, fmt.Errorf("failed to create contract: %v", err) + } + owner, err := c.Owner(nil) + if err != nil { + return common.Address{}, nil, fmt.Errorf("failed to get owner of contract: %v", err) + } + return owner, c, nil +} + +func (t TransferToMCMSWithTimelockConfig) Validate(e deployment.Environment) error { + for chainSelector, contracts := range t.ContractsByChain { + for _, contract := range contracts { + // Cannot transfer an unknown address. + // Note this also assures non-zero addresses. + if exists, err := deployment.AddressBookContains(e.ExistingAddresses, chainSelector, contract.String()); err != nil || !exists { + if err != nil { + return fmt.Errorf("failed to check address book: %v", err) + } + return fmt.Errorf("contract %s not found in address book", contract) + } + owner, _, err := LoadOwnableContract(contract, e.Chains[chainSelector].Client) + if err != nil { + return fmt.Errorf("failed to load ownable: %v", err) + } + if owner != e.Chains[chainSelector].DeployerKey.From { + return fmt.Errorf("contract %s is not owned by the deployer key", contract) + } + } + // If there is no timelock and mcms proposer on the chain, the transfer will fail. + if _, err := deployment.SearchAddressBook(e.ExistingAddresses, chainSelector, types.RBACTimelock); err != nil { + return fmt.Errorf("timelock not present on the chain %v", err) + } + if _, err := deployment.SearchAddressBook(e.ExistingAddresses, chainSelector, types.ProposerManyChainMultisig); err != nil { + return fmt.Errorf("mcms proposer not present on the chain %v", err) + } + } + + return nil +} + +var _ deployment.ChangeSet[TransferToMCMSWithTimelockConfig] = TransferToMCMSWithTimelock + +// TransferToMCMSWithTimelock creates a changeset that transfers ownership of all the +// contracts in the provided configuration to the timelock on the chain and generates +// a corresponding accept ownership proposal to complete the transfer. +// It assumes that DeployMCMSWithTimelock has already been run s.t. +// the timelock and mcmses exist on the chain and that the proposed addresses to transfer ownership +// are currently owned by the deployer key. +func TransferToMCMSWithTimelock( + e deployment.Environment, + cfg TransferToMCMSWithTimelockConfig, +) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, err + } + var batches []timelock.BatchChainOperation + timelocksByChain := make(map[uint64]common.Address) + proposersByChain := make(map[uint64]*owner_helpers.ManyChainMultiSig) + for chainSelector, contracts := range cfg.ContractsByChain { + // Already validated that the timelock/proposer exists. + timelockAddr, _ := deployment.SearchAddressBook(e.ExistingAddresses, chainSelector, types.RBACTimelock) + proposerAddr, _ := deployment.SearchAddressBook(e.ExistingAddresses, chainSelector, types.ProposerManyChainMultisig) + timelocksByChain[chainSelector] = common.HexToAddress(timelockAddr) + proposer, err := owner_helpers.NewManyChainMultiSig(common.HexToAddress(proposerAddr), e.Chains[chainSelector].Client) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to create proposer mcms: %v", err) + } + proposersByChain[chainSelector] = proposer + + var ops []mcms.Operation + for _, contract := range contracts { + // Just using the ownership interface. + // Already validated is ownable. + owner, c, _ := LoadOwnableContract(contract, e.Chains[chainSelector].Client) + if owner.String() == timelockAddr { + // Already owned by timelock. + e.Logger.Infof("contract %s already owned by timelock", contract) + continue + } + tx, err := c.TransferOwnership(e.Chains[chainSelector].DeployerKey, common.HexToAddress(timelockAddr)) + _, err = deployment.ConfirmIfNoError(e.Chains[chainSelector], tx, err) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to transfer ownership of contract %T: %v", contract, err) + } + tx, err = c.AcceptOwnership(deployment.SimTransactOpts()) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to generate accept ownership calldata of %s: %w", contract, err) + } + ops = append(ops, mcms.Operation{ + To: contract, + Data: tx.Data(), + Value: big.NewInt(0), + }) + } + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(chainSelector), + Batch: ops, + }) + } + proposal, err := proposalutils.BuildProposalFromBatches( + timelocksByChain, proposersByChain, batches, "Transfer ownership to timelock", cfg.MinDelay) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal from batch: %w, batches: %+v", err, batches) + } + + return deployment.ChangesetOutput{Proposals: []timelock.MCMSWithTimelockProposal{*proposal}}, nil +} diff --git a/deployment/common/changeset/transfer_to_mcms_with_timelock_test.go b/deployment/common/changeset/transfer_to_mcms_with_timelock_test.go new file mode 100644 index 00000000000..f1f24cb0b05 --- /dev/null +++ b/deployment/common/changeset/transfer_to_mcms_with_timelock_test.go @@ -0,0 +1,69 @@ +package changeset + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + "github.com/stretchr/testify/require" + + "math/big" + + "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func TestTransferToMCMSWithTimelock(t *testing.T) { + lggr := logger.TestLogger(t) + e := memory.NewMemoryEnvironment(t, lggr, 0, memory.MemoryEnvironmentConfig{ + Chains: 1, + Nodes: 1, + }) + chain1 := e.AllChainSelectors()[0] + e, err := ApplyChangesets(t, e, nil, []ChangesetApplication{ + { + Changeset: WrapChangeSet(DeployLinkToken), + Config: []uint64{chain1}, + }, + { + Changeset: WrapChangeSet(DeployMCMSWithTimelock), + Config: map[uint64]types.MCMSWithTimelockConfig{ + chain1: { + Canceller: SingleGroupMCMS(t), + Bypasser: SingleGroupMCMS(t), + Proposer: SingleGroupMCMS(t), + TimelockExecutors: e.AllDeployerKeys(), + TimelockMinDelay: big.NewInt(0), + }, + }, + }, + }) + require.NoError(t, err) + addrs, err := e.ExistingAddresses.AddressesForChain(chain1) + require.NoError(t, err) + state, err := LoadMCMSWithTimelockState(e.Chains[chain1], addrs) + require.NoError(t, err) + link, err := LoadLinkTokenState(e.Chains[chain1], addrs) + require.NoError(t, err) + e, err = ApplyChangesets(t, e, map[uint64]*owner_helpers.RBACTimelock{ + chain1: state.Timelock, + }, []ChangesetApplication{ + { + Changeset: WrapChangeSet(TransferToMCMSWithTimelock), + Config: TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chain1: {link.LinkToken.Address()}, + }, + MinDelay: 0, + }, + }, + }) + require.NoError(t, err) + // We expect now that the link token is owned by the MCMS timelock. + link, err = LoadLinkTokenState(e.Chains[chain1], addrs) + require.NoError(t, err) + o, err := link.LinkToken.Owner(nil) + require.NoError(t, err) + require.Equal(t, state.Timelock.Address(), o) +} diff --git a/deployment/common/proposalutils/propose.go b/deployment/common/proposalutils/propose.go index b4cb54af891..f525c0b6643 100644 --- a/deployment/common/proposalutils/propose.go +++ b/deployment/common/proposalutils/propose.go @@ -34,7 +34,7 @@ func buildProposalMetadata( return metaDataPerChain, nil } -// Given batches of operations, we build the metadata and timelock addresses of those opartions +// BuildProposalFromBatches Given batches of operations, we build the metadata and timelock addresses of those opartions // We then return a proposal that can be executed and signed func BuildProposalFromBatches( timelocksPerChain map[uint64]common.Address, diff --git a/deployment/common/view/v1_0/link_token.go b/deployment/common/view/v1_0/link_token.go new file mode 100644 index 00000000000..38649037592 --- /dev/null +++ b/deployment/common/view/v1_0/link_token.go @@ -0,0 +1,43 @@ +package v1_0 + +import ( + "math/big" + + "github.com/smartcontractkit/chainlink/deployment" + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/common/view/types" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/link_token" +) + +type LinkTokenView struct { + types.ContractMetaData + Decimals uint8 `json:"decimals"` + Supply *big.Int `json:"supply"` +} + +func GenerateLinkTokenView(lt *link_token.LinkToken) (LinkTokenView, error) { + owner, err := lt.Owner(nil) + if err != nil { + return LinkTokenView{}, err + } + decimals, err := lt.Decimals(nil) + if err != nil { + return LinkTokenView{}, err + } + totalSupply, err := lt.TotalSupply(nil) + if err != nil { + return LinkTokenView{}, err + } + return LinkTokenView{ + ContractMetaData: types.ContractMetaData{ + TypeAndVersion: deployment.TypeAndVersion{ + commontypes.LinkToken, + deployment.Version1_0_0, + }.String(), + Address: lt.Address(), + Owner: owner, + }, + Decimals: decimals, + Supply: totalSupply, + }, nil +} diff --git a/deployment/environment/memory/environment.go b/deployment/environment/memory/environment.go index 83346a60602..f4692998d34 100644 --- a/deployment/environment/memory/environment.go +++ b/deployment/environment/memory/environment.go @@ -101,6 +101,9 @@ func generateMemoryChain(t *testing.T, inputs map[uint64]EVMChain) map[uint64]de func NewNodes(t *testing.T, logLevel zapcore.Level, chains map[uint64]deployment.Chain, numNodes, numBootstraps int, registryConfig deployment.CapabilityRegistryConfig) map[string]Node { nodesByPeerID := make(map[string]Node) + if numNodes+numBootstraps == 0 { + return nodesByPeerID + } ports := freeport.GetN(t, numBootstraps+numNodes) // bootstrap nodes must be separate nodes from plugin nodes, // since we won't run a bootstrapper and a plugin oracle on the same diff --git a/deployment/keystone/changeset/accept_ownership.go b/deployment/keystone/changeset/accept_ownership.go index 8a4f3c60c53..7dffc5a70c4 100644 --- a/deployment/keystone/changeset/accept_ownership.go +++ b/deployment/keystone/changeset/accept_ownership.go @@ -5,20 +5,10 @@ import ( "github.com/ethereum/go-ethereum/common" - ccipowner "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" - "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/common/changeset" ) -func toOwnershipAcceptors[T changeset.OwnershipAcceptor](items []T) []changeset.OwnershipAcceptor { - ownershipAcceptors := make([]changeset.OwnershipAcceptor, len(items)) - for i, item := range items { - ownershipAcceptors[i] = item - } - return ownershipAcceptors -} - type AcceptAllOwnershipRequest struct { ChainSelector uint64 MinDelay time.Duration @@ -33,11 +23,6 @@ func AcceptAllOwnershipsProposal(e deployment.Environment, req *AcceptAllOwnersh chain := e.Chains[chainSelector] addrBook := e.ExistingAddresses - // Fetch contracts from the address book. - timelocks, err := timelocksFromAddrBook(addrBook, chain) - if err != nil { - return deployment.ChangesetOutput{}, err - } capRegs, err := capRegistriesFromAddrBook(addrBook, chain) if err != nil { return deployment.ChangesetOutput{}, err @@ -54,36 +39,27 @@ func AcceptAllOwnershipsProposal(e deployment.Environment, req *AcceptAllOwnersh if err != nil { return deployment.ChangesetOutput{}, err } - mcmsProposers, err := proposersFromAddrBook(addrBook, chain) - if err != nil { - return deployment.ChangesetOutput{}, err + var addrsToTransfer []common.Address + for _, consumer := range consumers { + addrsToTransfer = append(addrsToTransfer, consumer.Address()) + } + for _, o := range ocr3 { + addrsToTransfer = append(addrsToTransfer, o.Address()) + } + for _, f := range forwarders { + addrsToTransfer = append(addrsToTransfer, f.Address()) + } + for _, c := range capRegs { + addrsToTransfer = append(addrsToTransfer, c.Address()) } - - // Initialize the OwnershipAcceptors slice - var ownershipAcceptors []changeset.OwnershipAcceptor - - // Append all contracts - ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(capRegs)...) - ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(ocr3)...) - ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(forwarders)...) - ownershipAcceptors = append(ownershipAcceptors, toOwnershipAcceptors(consumers)...) - // Construct the configuration - cfg := changeset.AcceptOwnershipConfig{ - OwnersPerChain: map[uint64]common.Address{ - // Assuming there is only one timelock per chain. - chainSelector: timelocks[0].Address(), - }, - ProposerMCMSes: map[uint64]*ccipowner.ManyChainMultiSig{ - // Assuming there is only one MCMS proposer per chain. - chainSelector: mcmsProposers[0], - }, - Contracts: map[uint64][]changeset.OwnershipAcceptor{ - chainSelector: ownershipAcceptors, + cfg := changeset.TransferToMCMSWithTimelockConfig{ + ContractsByChain: map[uint64][]common.Address{ + chainSelector: addrsToTransfer, }, MinDelay: minDelay, } // Create and return the changeset - return changeset.NewAcceptOwnershipChangeset(e, cfg) + return changeset.TransferToMCMSWithTimelock(e, cfg) } diff --git a/deployment/keystone/changeset/accept_ownership_test.go b/deployment/keystone/changeset/accept_ownership_test.go index 996ff08c149..997f0b7e163 100644 --- a/deployment/keystone/changeset/accept_ownership_test.go +++ b/deployment/keystone/changeset/accept_ownership_test.go @@ -3,12 +3,13 @@ package changeset_test import ( "math/big" "testing" - "time" - "github.com/smartcontractkit/chainlink-common/pkg/logger" + owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/deployment/environment/memory" @@ -24,63 +25,52 @@ func TestAcceptAllOwnership(t *testing.T) { } env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) registrySel := env.AllChainSelectors()[0] - chCapReg, err := changeset.DeployCapabilityRegistry(env, registrySel) - require.NoError(t, err) - require.NotNil(t, chCapReg) - err = env.ExistingAddresses.Merge(chCapReg.AddressBook) - require.NoError(t, err) - - chOcr3, err := changeset.DeployOCR3(env, registrySel) - require.NoError(t, err) - require.NotNil(t, chOcr3) - err = env.ExistingAddresses.Merge(chOcr3.AddressBook) - require.NoError(t, err) - - chForwarder, err := changeset.DeployForwarder(env, registrySel) - require.NoError(t, err) - require.NotNil(t, chForwarder) - err = env.ExistingAddresses.Merge(chForwarder.AddressBook) - require.NoError(t, err) - - chConsumer, err := changeset.DeployFeedsConsumer(env, &changeset.DeployFeedsConsumerRequest{ - ChainSelector: registrySel, - }) - require.NoError(t, err) - require.NotNil(t, chConsumer) - err = env.ExistingAddresses.Merge(chConsumer.AddressBook) - require.NoError(t, err) - - chMcms, err := commonchangeset.DeployMCMSWithTimelock(env, map[uint64]types.MCMSWithTimelockConfig{ - registrySel: { - Canceller: commonchangeset.SingleGroupMCMS(t), - Bypasser: commonchangeset.SingleGroupMCMS(t), - Proposer: commonchangeset.SingleGroupMCMS(t), - TimelockExecutors: env.AllDeployerKeys(), - TimelockMinDelay: big.NewInt(0), + env, err := commonchangeset.ApplyChangesets(t, env, nil, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.DeployCapabilityRegistry), + Config: registrySel, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.DeployOCR3), + Config: registrySel, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.DeployForwarder), + Config: registrySel, + }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.DeployFeedsConsumer), + Config: &changeset.DeployFeedsConsumerRequest{ChainSelector: registrySel}, + }, + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployMCMSWithTimelock), + Config: map[uint64]types.MCMSWithTimelockConfig{ + registrySel: { + Canceller: commonchangeset.SingleGroupMCMS(t), + Bypasser: commonchangeset.SingleGroupMCMS(t), + Proposer: commonchangeset.SingleGroupMCMS(t), + TimelockExecutors: env.AllDeployerKeys(), + TimelockMinDelay: big.NewInt(0), + }, + }, }, }) - err = env.ExistingAddresses.Merge(chMcms.AddressBook) require.NoError(t, err) - + addrs, err := env.ExistingAddresses.AddressesForChain(registrySel) require.NoError(t, err) - require.NotNil(t, chMcms) - - resp, err := changeset.TransferAllOwnership(env, &changeset.TransferAllOwnershipRequest{ - ChainSelector: registrySel, - }) + timelock, err := commonchangeset.LoadMCMSWithTimelockState(env.Chains[registrySel], addrs) require.NoError(t, err) - require.NotNil(t, resp) - // Test the changeset - output, err := changeset.AcceptAllOwnershipsProposal(env, &changeset.AcceptAllOwnershipRequest{ - ChainSelector: registrySel, - MinDelay: time.Duration(0), + _, err = commonchangeset.ApplyChangesets(t, env, map[uint64]*owner_helpers.RBACTimelock{ + registrySel: timelock.Timelock, + }, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.AcceptAllOwnershipsProposal), + Config: &changeset.AcceptAllOwnershipRequest{ + ChainSelector: registrySel, + MinDelay: 0, + }, + }, }) require.NoError(t, err) - require.NotNil(t, output) - require.Len(t, output.Proposals, 1) - proposal := output.Proposals[0] - require.Len(t, proposal.Transactions, 1) - txs := proposal.Transactions[0] - require.Len(t, txs.Batch, 4) } diff --git a/deployment/keystone/changeset/deploy_ocr3.go b/deployment/keystone/changeset/deploy_ocr3.go index a88fe4afa62..40d9e558584 100644 --- a/deployment/keystone/changeset/deploy_ocr3.go +++ b/deployment/keystone/changeset/deploy_ocr3.go @@ -9,12 +9,10 @@ import ( kslib "github.com/smartcontractkit/chainlink/deployment/keystone" ) -func DeployOCR3(env deployment.Environment, config interface{}) (deployment.ChangesetOutput, error) { +var _ deployment.ChangeSet[uint64] = DeployOCR3 + +func DeployOCR3(env deployment.Environment, registryChainSel uint64) (deployment.ChangesetOutput, error) { lggr := env.Logger - registryChainSel, ok := config.(uint64) - if !ok { - return deployment.ChangesetOutput{}, deployment.ErrInvalidConfig - } ab := deployment.NewMemoryAddressBook() // ocr3 only deployed on registry chain c, ok := env.Chains[registryChainSel] diff --git a/deployment/keystone/changeset/transfer_ownership.go b/deployment/keystone/changeset/transfer_ownership.go deleted file mode 100644 index 73af5d5bdb2..00000000000 --- a/deployment/keystone/changeset/transfer_ownership.go +++ /dev/null @@ -1,84 +0,0 @@ -package changeset - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/deployment/common/changeset" -) - -func toOwnershipTransferrer[T changeset.OwnershipTransferrer](items []T) []changeset.OwnershipTransferrer { - ownershipAcceptors := make([]changeset.OwnershipTransferrer, len(items)) - for i, item := range items { - ownershipAcceptors[i] = item - } - return ownershipAcceptors -} - -type TransferAllOwnershipRequest struct { - ChainSelector uint64 -} - -var _ deployment.ChangeSet[*TransferAllOwnershipRequest] = TransferAllOwnership - -// TransferAllOwnership transfers ownership of all Keystone contracts in the address book to the existing timelock. -func TransferAllOwnership(e deployment.Environment, req *TransferAllOwnershipRequest) (deployment.ChangesetOutput, error) { - chainSelector := req.ChainSelector - chain := e.Chains[chainSelector] - addrBook := e.ExistingAddresses - - // Fetch timelocks for the specified chain. - timelocks, err := timelocksFromAddrBook(addrBook, chain) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch timelocks: %w", err) - } - if len(timelocks) == 0 { - return deployment.ChangesetOutput{}, fmt.Errorf("no timelocks found for chain %d", chainSelector) - } - - // Fetch contracts from the address book. - capRegs, err := capRegistriesFromAddrBook(addrBook, chain) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch capabilities registries: %w", err) - } - - ocr3s, err := ocr3FromAddrBook(addrBook, chain) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch OCR3 capabilities: %w", err) - } - - forwarders, err := forwardersFromAddrBook(addrBook, chain) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch forwarders: %w", err) - } - - consumers, err := feedsConsumersFromAddrBook(addrBook, chain) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("failed to fetch feeds consumers: %w", err) - } - - // Initialize the Contracts slice - var ownershipTransferrers []changeset.OwnershipTransferrer - - // Append all contracts - ownershipTransferrers = append(ownershipTransferrers, toOwnershipTransferrer(capRegs)...) - ownershipTransferrers = append(ownershipTransferrers, toOwnershipTransferrer(ocr3s)...) - ownershipTransferrers = append(ownershipTransferrers, toOwnershipTransferrer(forwarders)...) - ownershipTransferrers = append(ownershipTransferrers, toOwnershipTransferrer(consumers)...) - - // Construct the configuration - cfg := changeset.TransferOwnershipConfig{ - OwnersPerChain: map[uint64]common.Address{ - // Assuming there is only one timelock per chain. - chainSelector: timelocks[0].Address(), - }, - Contracts: map[uint64][]changeset.OwnershipTransferrer{ - chainSelector: ownershipTransferrers, - }, - } - - // Create and return the changeset - return changeset.NewTransferOwnershipChangeset(e, cfg) -} diff --git a/deployment/keystone/changeset/transfer_ownership_test.go b/deployment/keystone/changeset/transfer_ownership_test.go deleted file mode 100644 index dc5630076bd..00000000000 --- a/deployment/keystone/changeset/transfer_ownership_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package changeset_test - -import ( - "math/big" - "testing" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/stretchr/testify/require" - "go.uber.org/zap/zapcore" - - commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" - "github.com/smartcontractkit/chainlink/deployment/common/types" - "github.com/smartcontractkit/chainlink/deployment/environment/memory" - "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" -) - -func TestTransferAllOwnership(t *testing.T) { - t.Parallel() - lggr := logger.Test(t) - cfg := memory.MemoryEnvironmentConfig{ - Nodes: 1, - Chains: 2, - } - env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) - registrySel := env.AllChainSelectors()[0] - chCapReg, err := changeset.DeployCapabilityRegistry(env, registrySel) - require.NoError(t, err) - require.NotNil(t, chCapReg) - err = env.ExistingAddresses.Merge(chCapReg.AddressBook) - require.NoError(t, err) - - chOcr3, err := changeset.DeployOCR3(env, registrySel) - require.NoError(t, err) - require.NotNil(t, chOcr3) - err = env.ExistingAddresses.Merge(chOcr3.AddressBook) - require.NoError(t, err) - - chForwarder, err := changeset.DeployForwarder(env, registrySel) - require.NoError(t, err) - require.NotNil(t, chForwarder) - err = env.ExistingAddresses.Merge(chForwarder.AddressBook) - require.NoError(t, err) - - chConsumer, err := changeset.DeployFeedsConsumer(env, &changeset.DeployFeedsConsumerRequest{ - ChainSelector: registrySel, - }) - require.NoError(t, err) - require.NotNil(t, chConsumer) - err = env.ExistingAddresses.Merge(chConsumer.AddressBook) - require.NoError(t, err) - - chMcms, err := commonchangeset.DeployMCMSWithTimelock(env, map[uint64]types.MCMSWithTimelockConfig{ - registrySel: { - Canceller: commonchangeset.SingleGroupMCMS(t), - Bypasser: commonchangeset.SingleGroupMCMS(t), - Proposer: commonchangeset.SingleGroupMCMS(t), - TimelockExecutors: env.AllDeployerKeys(), - TimelockMinDelay: big.NewInt(0), - }, - }) - err = env.ExistingAddresses.Merge(chMcms.AddressBook) - require.NoError(t, err) - - require.NoError(t, err) - require.NotNil(t, chMcms) - - resp, err := changeset.TransferAllOwnership(env, &changeset.TransferAllOwnershipRequest{ - ChainSelector: registrySel, - }) - require.NoError(t, err) - require.NotNil(t, resp) -} diff --git a/integration-tests/testsetups/ccip/test_helpers.go b/integration-tests/testsetups/ccip/test_helpers.go index fc9925372bc..4d51093d419 100644 --- a/integration-tests/testsetups/ccip/test_helpers.go +++ b/integration-tests/testsetups/ccip/test_helpers.go @@ -169,6 +169,10 @@ func NewLocalDevEnvironment( }, }, }, + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployLinkToken), + Config: allChains, + }, { Changeset: commonchangeset.WrapChangeSet(changeset.DeployPrerequisites), Config: changeset.DeployPrerequisiteConfig{