Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions legacy/mcms/changesets/set_config_mcms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
solstate "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana"
"github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/changesets"
soltestutils "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/testutils"
linkchangesets "github.com/smartcontractkit/cld-changesets/link/changesets"
linkchangesets "github.com/smartcontractkit/cld-changesets/tokens/link/changesets"

cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"

Expand Down Expand Up @@ -300,7 +300,9 @@ func TestValidateV2(t *testing.T) {

// Deploy MCMS and Timelock
err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(linkchangesets.DeployLinkToken), []uint64{evmSelector}),
runtime.ChangesetTask(linkchangesets.DeployLinkTokenChangeset{}, linkchangesets.DeployLinkTokenInput{
EVM: map[uint64]linkchangesets.EVMLinkConfig{evmSelector: {}},
}),
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(DeployMCMSWithTimelockV2), map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
evmSelector: config,
solSelector: config,
Expand Down
69 changes: 13 additions & 56 deletions legacy/mcms/changesets/transfer_to_mcms_with_timelock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,17 @@ import (
"github.com/ethereum/go-ethereum/common"
chain_selectors "github.com/smartcontractkit/chain-selectors"
cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
linkcontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/link"
"github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/link_token"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/cld-changesets/internal/semvers"
evmstate "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/evm"
linkchangesets "github.com/smartcontractkit/cld-changesets/link/changesets"
linkchangesets "github.com/smartcontractkit/cld-changesets/tokens/link/changesets"

cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"

"github.com/smartcontractkit/chainlink-deployments-framework/datastore"

"github.com/smartcontractkit/chainlink-common/pkg/utils/tests"

cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
Expand All @@ -42,9 +41,10 @@ func TestTransferToMCMSWithTimelockV2(t *testing.T) {

chain := rt.Environment().BlockChains.EVMChains()[selector]

// Setup contracts
err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(linkchangesets.DeployLinkToken), []uint64{selector}),
runtime.ChangesetTask(linkchangesets.DeployLinkTokenChangeset{}, linkchangesets.DeployLinkTokenInput{
EVM: map[uint64]linkchangesets.EVMLinkConfig{selector: {}},
}),
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(DeployMCMSWithTimelockV2), map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
selector: cldftesthelpers.SingleGroupTimelockConfig(t),
}),
Expand All @@ -57,7 +57,7 @@ func TestTransferToMCMSWithTimelockV2(t *testing.T) {
state, err := evmstate.MaybeLoadMCMSWithTimelockChainState(chain, addrs)
require.NoError(t, err)

linkToken, err := loadLinkTokenFromAddressBook(chain, addrs)
linkToken, err := loadLinkTokenFromDataStore(chain, rt.State().DataStore)
require.NoError(t, err)

err = rt.Exec(
Expand All @@ -75,15 +75,10 @@ func TestTransferToMCMSWithTimelockV2(t *testing.T) {
require.Len(t, rt.State().Proposals, 1)
require.True(t, rt.State().Proposals[0].IsExecuted)

// We expect now that the link token is owned by the MCMS timelock.
linkToken, err = loadLinkTokenFromAddressBook(chain, addrs)
require.NoError(t, err)

o, err := linkToken.Owner(nil)
require.NoError(t, err)
require.Equal(t, state.Timelock.Address(), o)

// Try a rollback to the deployer.
err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(TransferToDeployer), TransferToDeployerConfig{
ContractAddress: linkToken.Address(),
Expand All @@ -108,9 +103,10 @@ func TestTransferToMCMSWithTimelockV2DataStore(t *testing.T) {

chain := rt.Environment().BlockChains.EVMChains()[selector]

// Setup contracts
err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(linkchangesets.DeployLinkToken), []uint64{selector}),
runtime.ChangesetTask(linkchangesets.DeployLinkTokenChangeset{}, linkchangesets.DeployLinkTokenInput{
EVM: map[uint64]linkchangesets.EVMLinkConfig{selector: {}},
}),
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(DeployMCMSWithTimelockV2), map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
selector: cldftesthelpers.SingleGroupTimelockConfig(t),
}),
Expand All @@ -126,16 +122,6 @@ func TestTransferToMCMSWithTimelockV2DataStore(t *testing.T) {
linkToken, err := loadLinkTokenFromDataStore(chain, rt.State().DataStore)
require.NoError(t, err)

// Remove LinkToken from AddressBook to simulate datastore only having the contract address
// TODO: migrate DeployLinkToken to use datastore only
linkAb := cldf.NewMemoryAddressBookFromMap(map[uint64]map[string]cldf.TypeAndVersion{
selector: {
linkToken.Address().String(): cldf.NewTypeAndVersion(linkcontracts.LinkToken, semvers.V1_0_0),
},
})
err = rt.State().AddressBook.Remove(linkAb)
require.NoError(t, err)

err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(TransferToMCMSWithTimelockV2), TransferToMCMSWithTimelockConfig{
ContractsByChain: map[uint64][]common.Address{
Expand All @@ -151,12 +137,10 @@ func TestTransferToMCMSWithTimelockV2DataStore(t *testing.T) {
require.Len(t, rt.State().Proposals, 1)
require.True(t, rt.State().Proposals[0].IsExecuted)

// We expect now that the link token is owned by the MCMS timelock.
o, err := linkToken.Owner(nil)
require.NoError(t, err)
require.Equal(t, state.Timelock.Address(), o)

// Try a rollback to the deployer.
err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(TransferToDeployer), TransferToDeployerConfig{
ContractAddress: linkToken.Address(),
Expand All @@ -181,7 +165,6 @@ func TestRenounceTimelockDeployerConfigValidate(t *testing.T) {
))
require.NoError(t, err)

// Deploy MCMS to selector 1 only, so we have a chain without MCMS
err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(DeployMCMSWithTimelockV2), map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
selector1: cldftesthelpers.SingleGroupTimelockConfig(t),
Expand Down Expand Up @@ -224,7 +207,6 @@ func TestRenounceTimelockDeployerConfigValidate(t *testing.T) {
config: RenounceTimelockDeployerConfig{
ChainSel: selector2,
},
// chain does not match any existing addresses
err: "timelock not found on chain 5548718428018410741",
},
} {
Expand Down Expand Up @@ -272,52 +254,27 @@ func TestRenounceTimelockDeployer(t *testing.T) { //nolint:paralleltest
require.NoError(t, err)
require.Equal(t, int64(2), r.Int64())

// Revoke Deployer
err = rt.Exec(
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(RenounceTimelockDeployer), RenounceTimelockDeployerConfig{
ChainSel: selector,
}),
)
require.NoError(t, err)

// Check that the deployer is no longer an admin
r, err = tl.GetRoleMemberCount(&bind.CallOpts{}, adminRole)
require.NoError(t, err)
require.Equal(t, int64(1), r.Int64())

// Retrieve the admin address
admin, err := tl.GetRoleMember(&bind.CallOpts{}, adminRole, big.NewInt(0))
require.NoError(t, err)

// Check that the admin is the timelock
require.Equal(t, tl.Address(), admin)
}

func loadLinkTokenFromAddressBook(chain cldf_evm.Chain, addresses map[string]cldf.TypeAndVersion) (*link_token.LinkToken, error) {
linkToken := cldf.NewTypeAndVersion(linkcontracts.LinkToken, semvers.V1_0_0)

// Convert map keys to a slice
wantTypes := []cldf.TypeAndVersion{linkToken}

// Ensure we either have the bundle or not.
_, err := cldf.EnsureDeduped(addresses, wantTypes)
if err != nil {
return nil, fmt.Errorf("unable to check link token on chain %s error: %w", chain.Name(), err)
}

for address, tvStr := range addresses {
if tvStr.Type == linkToken.Type && tvStr.Version.String() == linkToken.Version.String() {
return link_token.NewLinkToken(common.HexToAddress(address), chain.Client)
}
}

return nil, fmt.Errorf("link token not found on chain %s", chain.Name())
}

func loadLinkTokenFromDataStore(chain cldf_evm.Chain, dataStore datastore.DataStore) (*link_token.LinkToken, error) {
linkToken := cldf.NewTypeAndVersion(linkcontracts.LinkToken, semvers.V1_0_0)
func loadLinkTokenFromDataStore(chain cldf_evm.Chain, ds datastore.DataStore) (*link_token.LinkToken, error) {
linkTokenTV := cldf.NewTypeAndVersion(linkcontracts.LinkToken, semvers.V1_0_0)

refs, err := dataStore.Addresses().Fetch()
refs, err := ds.Addresses().Fetch()
if err != nil {
return nil, err
}
Expand All @@ -327,7 +284,7 @@ func loadLinkTokenFromDataStore(chain cldf_evm.Chain, dataStore datastore.DataSt
continue
}

if ref.Type == datastore.ContractType(linkToken.Type.String()) && ref.Version.String() == linkToken.Version.String() {
if ref.Type == datastore.ContractType(linkTokenTV.Type.String()) && ref.Version != nil && ref.Version.String() == linkTokenTV.Version.String() {
return link_token.NewLinkToken(common.HexToAddress(ref.Address), chain.Client)
}
}
Expand Down
170 changes: 170 additions & 0 deletions tokens/link/changesets/deploy_link_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Package changesets provides reusable LINK token changesets.
package changesets

import (
"errors"
"fmt"

"github.com/gagliardetto/solana-go"
chainsel "github.com/smartcontractkit/chain-selectors"
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations"

opsevm "github.com/smartcontractkit/cld-changesets/pkg/family/evm/operations"
linkops "github.com/smartcontractkit/cld-changesets/tokens/link/operations"
)

var _ cldf.ChangeSetV2[DeployLinkTokenInput] = DeployLinkTokenChangeset{}

// EVMLinkVariant selects the EVM LINK token contract to deploy.
// The zero value deploys the burn/mint ERC677 variant (the default for all modern chains).
type EVMLinkVariant string

const (
// EVMLinkBurnMint deploys a burn/mint ERC677 LINK token. This is the default.
EVMLinkBurnMint EVMLinkVariant = ""
// EVMLinkStatic deploys a non-burn/mint static LINK token for chains that do not
// support the burn/mint interface.
EVMLinkStatic EVMLinkVariant = "static"
)

// EVMLinkConfig holds per-chain configuration for EVM LINK token deployment.
type EVMLinkConfig struct {
Variant EVMLinkVariant `yaml:"variant,omitempty" json:"variant,omitempty"`
Qualifier string `yaml:"qualifier,omitempty" json:"qualifier,omitempty"`
}

// SolanaLinkConfig holds per-chain configuration for Solana LINK token deployment.
type SolanaLinkConfig struct {
TokenPrivKey solana.PrivateKey `yaml:"tokenPrivKey" json:"tokenPrivKey"`
TokenDecimals uint8 `yaml:"tokenDecimals" json:"tokenDecimals"`
Qualifier string `yaml:"qualifier,omitempty" json:"qualifier,omitempty"`
}

// DeployLinkTokenInput specifies which chains to deploy LINK tokens to and how.
type DeployLinkTokenInput struct {
EVM map[uint64]EVMLinkConfig `yaml:"evm,omitempty" json:"evm,omitempty"`
Solana map[uint64]SolanaLinkConfig `yaml:"solana,omitempty" json:"solana,omitempty"`
}

// DeployLinkTokenChangeset deploys LINK tokens across EVM and Solana chains.
type DeployLinkTokenChangeset struct{}

func (DeployLinkTokenChangeset) VerifyPreconditions(e cldf.Environment, input DeployLinkTokenInput) error {
if len(input.EVM) == 0 && len(input.Solana) == 0 {
return errors.New("no chains specified: at least one EVM or Solana chain is required")
}

for sel, cfg := range input.EVM {
if !e.BlockChains.Exists(sel) {
return fmt.Errorf("EVM chain %d not found in environment", sel)
}

if err := validateSelectorsFamily([]uint64{sel}, chainsel.FamilyEVM); err != nil {
return err
}

if cfg.Variant != EVMLinkBurnMint && cfg.Variant != EVMLinkStatic {
return fmt.Errorf("unknown EVM LINK variant %q for chain %d: must be %q or %q", cfg.Variant, sel, EVMLinkBurnMint, EVMLinkStatic)
}

tv := linkTokenTypeAndVersion()
if cfg.Variant == EVMLinkStatic {
tv = staticLinkTokenTypeAndVersion()
}

if err := validateNoExistingContract(e, []uint64{sel}, tv, cfg.Qualifier); err != nil {
return err
}
}

for sel, cfg := range input.Solana {
if !e.BlockChains.Exists(sel) {
return fmt.Errorf("solana chain %d not found in environment", sel)
}

if err := validateSelectorsFamily([]uint64{sel}, chainsel.FamilySolana); err != nil {
return err
}

if len(cfg.TokenPrivKey) == 0 {
return fmt.Errorf("solana chain %d: TokenPrivKey must be set", sel)
}

if err := validateNoExistingContract(e, []uint64{sel}, linkTokenTypeAndVersion(), cfg.Qualifier); err != nil {
return err
}
}

return nil
}

func (DeployLinkTokenChangeset) Apply(e cldf.Environment, input DeployLinkTokenInput) (cldf.ChangesetOutput, error) {
ds := datastore.NewMemoryDataStore()
allReports := make([]cldfops.Report[any, any], 0, len(input.EVM)+len(input.Solana))

for sel, cfg := range input.EVM {
Comment thread
ChrisAmora marked this conversation as resolved.
Comment thread
ChrisAmora marked this conversation as resolved.
chain, ok := e.BlockChains.EVMChains()[sel]
if !ok {
return cldf.ChangesetOutput{}, fmt.Errorf("EVM chain not found in environment: %d", sel)
}

op := linkops.OpEVMDeployLinkToken
tv := linkTokenTypeAndVersion()
if cfg.Variant == EVMLinkStatic {
op = linkops.OpEVMDeployStaticLinkToken
tv = staticLinkTokenTypeAndVersion()
}

qualifier := cfg.Qualifier
report, err := cldfops.ExecuteOperation(
e.OperationsBundle,
op,
chain,
opsevm.EVMDeployInput[any]{ChainSelector: sel, Qualifier: &qualifier},
)
if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to deploy link token for chain %d: %w", sel, err)
}

addr := report.Output.Address.String()
if err := saveAddressRef(ds, sel, addr, tv, cfg.Qualifier); err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to save link token address for chain %d: %w", sel, err)
}

allReports = append(allReports, report.ToGenericReport())
e.Logger.Infow("Deployed link token", "chain", sel, "addr", addr, "variant", tv.Type)
}

tv := linkTokenTypeAndVersion()
for sel, cfg := range input.Solana {
Comment thread
ChrisAmora marked this conversation as resolved.
chain, ok := e.BlockChains.SolanaChains()[sel]
if !ok {
return cldf.ChangesetOutput{}, fmt.Errorf("solana chain not found in environment: %d", sel)
}

report, err := cldfops.ExecuteOperation(
e.OperationsBundle,
linkops.OpSolanaDeployLinkToken,
chain,
linkops.SolanaLinkDeployInput{
MintKey: cfg.TokenPrivKey,
Decimals: cfg.TokenDecimals,
},
)
if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to deploy Solana link token for chain %d: %w", sel, err)
}

addr := report.Output.Address
if err := saveAddressRef(ds, sel, addr, tv, cfg.Qualifier); err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to save Solana link token address for chain %d: %w", sel, err)
}

allReports = append(allReports, report.ToGenericReport())
e.Logger.Infow("Deployed Solana link token", "chain", sel, "addr", addr)
}

return cldf.ChangesetOutput{DataStore: ds, Reports: allReports}, nil
}
Loading
Loading