Skip to content

Commit

Permalink
feat: link transfer mcms changesets (#15512)
Browse files Browse the repository at this point in the history
* feat: link transfer with timelock changeset

* feat: link transfer and approval integration tests and changesets.

* feat: rename files

* fix: use deployment.SimTransactOpts() to get tx data

* fix: link contract creation

* fix: remove approval changeset, not necessary for sending directly from the owner

* feat: make config accept a map of chain selectors for the proposal generation

* fix: params on deploy link

* fix: simplify config args by using state helper functions.

* fix: use pointer for value

* feat: add mint permissions and minting link changeset

* Deploy call proxy instead of using deployer executor keys

* inject call proxies in execution methods

* skip call proxy when loading chain state

* revert all changes

* Revert "revert all changes"

This reverts commit c17911e.

* chore: rename load state funcs

* feat: add mcms config flag

* fix: integration tests after merging develop

* fix: use contracts from states and code improvements

* fix: cs deploy chain args

* fix: params ccip boosting

* fix: bundle mcms config into single struct

* fix: add more validations for config

* fix: remove startingOpCount and use proposal utils to derive it

* fix: adjust variable names, remove boolean for mcms config, add constants move balance validation to Validate() function

* feat: add tests for non mcms case, improve validations, and wait for tx confirmation.

* feat: check valid until is in future

* feat: add tests for Validate() and small validation fixes

* fix: rename MaybeLoadLinkTokenState to MaybeLoadLinkTokenChainState to abstract loading state per chain

* Update deployment/common/changeset/example/link_transfer.go

Co-authored-by: Graham Goh <[email protected]>

* fix: error handling and validations

* fix: use getDeployer helper

* feat: split mint burners into a separate changeset

* fix: name TestMintLink on unit test

* Update deployment/common/changeset/example/add_mint_burners_link.go

Co-authored-by: Graham Goh <[email protected]>

* Update deployment/common/changeset/example/add_mint_burners_link.go

Co-authored-by: Graham Goh <[email protected]>

* fix: use changeset apply for unit tests environment setup

* fix: linting errors

* fix: merge conflicts

* fix: remove valid unit to reuse util for proposal creation

---------

Co-authored-by: Akhil Chainani <[email protected]>
Co-authored-by: Graham Goh <[email protected]>
  • Loading branch information
3 people authored Dec 13, 2024
1 parent 7ca4930 commit 97b0563
Show file tree
Hide file tree
Showing 16 changed files with 955 additions and 25 deletions.
4 changes: 2 additions & 2 deletions deployment/ccip/changeset/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,13 +311,13 @@ func LoadOnchainState(e deployment.Environment) (CCIPOnChainState, error) {
// LoadChainState Loads all state for a chain into state
func LoadChainState(chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (CCIPChainState, error) {
var state CCIPChainState
mcmsWithTimelock, err := commoncs.MaybeLoadMCMSWithTimelockState(chain, addresses)
mcmsWithTimelock, err := commoncs.MaybeLoadMCMSWithTimelockChainState(chain, addresses)
if err != nil {
return state, err
}
state.MCMSWithTimelockState = *mcmsWithTimelock

linkState, err := commoncs.MaybeLoadLinkTokenState(chain, addresses)
linkState, err := commoncs.MaybeLoadLinkTokenChainState(chain, addresses)
if err != nil {
return state, err
}
Expand Down
2 changes: 1 addition & 1 deletion deployment/common/changeset/deploy_link_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

var _ deployment.ChangeSet[[]uint64] = DeployLinkToken

// DeployLinkToken deploys a link token contract to the chain identified by the chainSelector.
// DeployLinkToken deploys a link token contract to the chain identified by the ChainSelector.
func DeployLinkToken(e deployment.Environment, chains []uint64) (deployment.ChangesetOutput, error) {
for _, chain := range chains {
_, ok := e.Chains[chain]
Expand Down
2 changes: 1 addition & 1 deletion deployment/common/changeset/deploy_link_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestDeployLinkToken(t *testing.T) {
require.NoError(t, err)
addrs, err := e.ExistingAddresses.AddressesForChain(chain1)
require.NoError(t, err)
state, err := changeset.MaybeLoadLinkTokenState(e.Chains[chain1], addrs)
state, err := changeset.MaybeLoadLinkTokenChainState(e.Chains[chain1], addrs)
require.NoError(t, err)
// View itself already unit tested
_, err = state.GenerateLinkView()
Expand Down
70 changes: 70 additions & 0 deletions deployment/common/changeset/example/add_mint_burners_link.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package example

import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"

"github.com/smartcontractkit/chainlink/deployment"
"github.com/smartcontractkit/chainlink/deployment/common/changeset"
)

type AddMintersBurnersLinkConfig struct {
ChainSelector uint64
Minters []common.Address
Burners []common.Address
}

var _ deployment.ChangeSet[*AddMintersBurnersLinkConfig] = AddMintersBurnersLink

// AddMintersBurnersLink grants the minter / burner role to the provided addresses.
func AddMintersBurnersLink(e deployment.Environment, cfg *AddMintersBurnersLinkConfig) (deployment.ChangesetOutput, error) {

chain := e.Chains[cfg.ChainSelector]
addresses, err := e.ExistingAddresses.AddressesForChain(cfg.ChainSelector)
if err != nil {
return deployment.ChangesetOutput{}, err
}
linkState, err := changeset.MaybeLoadLinkTokenChainState(chain, addresses)
if err != nil {
return deployment.ChangesetOutput{}, err
}

for _, minter := range cfg.Minters {
// check if minter is already a minter
isMinter, err := linkState.LinkToken.IsMinter(&bind.CallOpts{Context: e.GetContext()}, minter)
if err != nil {
return deployment.ChangesetOutput{}, err
}
if isMinter {
continue
}
tx, err := linkState.LinkToken.GrantMintRole(chain.DeployerKey, minter)
if err != nil {
return deployment.ChangesetOutput{}, err
}
_, err = deployment.ConfirmIfNoError(chain, tx, err)
if err != nil {
return deployment.ChangesetOutput{}, err
}
}
for _, burner := range cfg.Burners {
// check if burner is already a burner
isBurner, err := linkState.LinkToken.IsBurner(&bind.CallOpts{Context: e.GetContext()}, burner)
if err != nil {
return deployment.ChangesetOutput{}, err
}
if isBurner {
continue
}
tx, err := linkState.LinkToken.GrantBurnRole(chain.DeployerKey, burner)
if err != nil {
return deployment.ChangesetOutput{}, err
}
_, err = deployment.ConfirmIfNoError(chain, tx, err)
if err != nil {
return deployment.ChangesetOutput{}, err
}
}
return deployment.ChangesetOutput{}, nil

}
50 changes: 50 additions & 0 deletions deployment/common/changeset/example/add_mint_burners_link_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package example_test

import (
"context"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink/deployment/common/changeset"
"github.com/smartcontractkit/chainlink/deployment/common/changeset/example"
)

// TestAddMintersBurnersLink tests the AddMintersBurnersLink changeset
func TestAddMintersBurnersLink(t *testing.T) {
t.Parallel()
ctx := context.Background()
// Deploy Link Token and Timelock contracts and add addresses to environment
env := setupLinkTransferTestEnv(t)

chainSelector := env.AllChainSelectors()[0]
chain := env.Chains[chainSelector]
addrs, err := env.ExistingAddresses.AddressesForChain(chainSelector)
require.NoError(t, err)
require.Len(t, addrs, 6)

mcmsState, err := changeset.MaybeLoadMCMSWithTimelockChainState(chain, addrs)
require.NoError(t, err)
linkState, err := changeset.MaybeLoadLinkTokenChainState(chain, addrs)
require.NoError(t, err)

timelockAddress := mcmsState.Timelock.Address()

// Mint some funds
_, err = example.AddMintersBurnersLink(env, &example.AddMintersBurnersLinkConfig{
ChainSelector: chainSelector,
Minters: []common.Address{timelockAddress},
Burners: []common.Address{timelockAddress},
})
require.NoError(t, err)

// check timelock balance
isMinter, err := linkState.LinkToken.IsMinter(&bind.CallOpts{Context: ctx}, timelockAddress)
require.NoError(t, err)
require.True(t, isMinter)
isBurner, err := linkState.LinkToken.IsBurner(&bind.CallOpts{Context: ctx}, timelockAddress)
require.NoError(t, err)
require.True(t, isBurner)
}
239 changes: 239 additions & 0 deletions deployment/common/changeset/example/link_transfer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package example

import (
"errors"
"fmt"
"math/big"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers"
chain_selectors "github.com/smartcontractkit/chain-selectors"

"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/changeset"
"github.com/smartcontractkit/chainlink/deployment/common/proposalutils"
"github.com/smartcontractkit/chainlink/deployment/common/types"
)

const MaxTimelockDelay = 24 * 7 * time.Hour

type TransferConfig struct {
To common.Address
Value *big.Int
}

type MCMSConfig struct {
MinDelay time.Duration // delay for timelock worker to execute the transfers.
OverrideRoot bool
}

type LinkTransferConfig struct {
Transfers map[uint64][]TransferConfig
From common.Address
McmsConfig *MCMSConfig
}

var _ deployment.ChangeSet[*LinkTransferConfig] = LinkTransfer

func getDeployer(e deployment.Environment, chain uint64, mcmConfig *MCMSConfig) *bind.TransactOpts {
if mcmConfig == nil {
return e.Chains[chain].DeployerKey
}

return deployment.SimTransactOpts()
}

// Validate checks that the LinkTransferConfig is valid.
func (cfg LinkTransferConfig) Validate(e deployment.Environment) error {
ctx := e.GetContext()
// Check that Transfers map has at least one chainSel
if len(cfg.Transfers) == 0 {
return errors.New("transfers map must have at least one chainSel")
}

// Check transfers config values.
for chainSel, transfers := range cfg.Transfers {
selector, err := chain_selectors.GetSelectorFamily(chainSel)
if err != nil {
return fmt.Errorf("invalid chain selector: %w", err)
}
if selector != chain_selectors.FamilyEVM {
return fmt.Errorf("chain selector %d is not an EVM chain", chainSel)
}
chain, ok := e.Chains[chainSel]
if !ok {
return fmt.Errorf("chain with selector %d not found", chainSel)
}
addrs, err := e.ExistingAddresses.AddressesForChain(chainSel)
if err != nil {
return fmt.Errorf("error getting addresses for chain %d: %w", chainSel, err)
}
if len(transfers) == 0 {
return fmt.Errorf("transfers for chainSel %d must have at least one LinkTransfer", chainSel)
}
totalAmount := big.NewInt(0)
linkState, err := changeset.MaybeLoadLinkTokenChainState(chain, addrs)
if err != nil {
return fmt.Errorf("error loading link token state during validation: %w", err)
}
for _, transfer := range transfers {
if transfer.To == (common.Address{}) {
return errors.New("'to' address for transfers must be set")
}
if transfer.Value == nil {
return errors.New("value for transfers must be set")
}
if transfer.Value.Cmp(big.NewInt(0)) == 0 {
return errors.New("value for transfers must be non-zero")
}
if transfer.Value.Cmp(big.NewInt(0)) == -1 {
return errors.New("value for transfers must be positive")
}
totalAmount.Add(totalAmount, transfer.Value)
}
// check that from address has enough funds for the transfers
balance, err := linkState.LinkToken.BalanceOf(&bind.CallOpts{Context: ctx}, cfg.From)
if balance.Cmp(totalAmount) < 0 {
return fmt.Errorf("sender does not have enough funds for transfers for chain selector %d, required: %s, available: %s", chainSel, totalAmount.String(), balance.String())
}
}

if cfg.McmsConfig == nil {
return nil
}

// Upper bound for min delay (7 days)
if cfg.McmsConfig.MinDelay > MaxTimelockDelay {
return errors.New("minDelay must be less than 7 days")
}

return nil
}

// initStatePerChain initializes the state for each chain selector on the provided config
func initStatePerChain(cfg *LinkTransferConfig, e deployment.Environment) (
linkStatePerChain map[uint64]*changeset.LinkTokenState,
mcmsStatePerChain map[uint64]*changeset.MCMSWithTimelockState,
err error) {
linkStatePerChain = map[uint64]*changeset.LinkTokenState{}
mcmsStatePerChain = map[uint64]*changeset.MCMSWithTimelockState{}
// Load state for each chain
chainSelectors := []uint64{}
for chainSelector := range cfg.Transfers {
chainSelectors = append(chainSelectors, chainSelector)
}
linkStatePerChain, err = changeset.MaybeLoadLinkTokenState(e, chainSelectors)
if err != nil {
return nil, nil, err
}
mcmsStatePerChain, err = changeset.MaybeLoadMCMSWithTimelockState(e, chainSelectors)
if err != nil {
return nil, nil, err

}
return linkStatePerChain, mcmsStatePerChain, nil
}

// transferOrBuildTx transfers the LINK tokens or builds the tx for the MCMS proposal
func transferOrBuildTx(
e deployment.Environment,
linkState *changeset.LinkTokenState,
transfer TransferConfig,
opts *bind.TransactOpts,
chain deployment.Chain,
mcmsConfig *MCMSConfig) (*ethTypes.Transaction, error) {
tx, err := linkState.LinkToken.Transfer(opts, transfer.To, transfer.Value)
if err != nil {
return nil, fmt.Errorf("error packing transfer tx data: %w", err)
}
// only wait for tx if we are not using MCMS
if mcmsConfig == nil {
if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil {
e.Logger.Errorw("Failed to confirm transfer tx", "chain", chain.String(), "err", err)
return nil, err
}
}
return tx, nil

}

// LinkTransfer takes the given link transfers and executes them or creates an MCMS proposal for them.
func LinkTransfer(e deployment.Environment, cfg *LinkTransferConfig) (deployment.ChangesetOutput, error) {

err := cfg.Validate(e)
if err != nil {
return deployment.ChangesetOutput{}, fmt.Errorf("invalid LinkTransferConfig: %w", err)
}
chainSelectors := []uint64{}
for chainSelector := range cfg.Transfers {
chainSelectors = append(chainSelectors, chainSelector)
}
mcmsPerChain := map[uint64]*owner_helpers.ManyChainMultiSig{}

timelockAddresses := map[uint64]common.Address{}
// Initialize state for each chain
linkStatePerChain, mcmsStatePerChain, err := initStatePerChain(cfg, e)

allBatches := []timelock.BatchChainOperation{}
for chainSelector := range cfg.Transfers {
chainID := mcms.ChainIdentifier(chainSelector)
chain := e.Chains[chainSelector]
linkAddress := linkStatePerChain[chainSelector].LinkToken.Address()
mcmsState := mcmsStatePerChain[chainSelector]
linkState := linkStatePerChain[chainSelector]

timelockAddress := mcmsState.Timelock.Address()

mcmsPerChain[uint64(chainID)] = mcmsState.ProposerMcm

timelockAddresses[chainSelector] = timelockAddress
batch := timelock.BatchChainOperation{
ChainIdentifier: chainID,
Batch: []mcms.Operation{},
}

opts := getDeployer(e, chainSelector, cfg.McmsConfig)
totalAmount := big.NewInt(0)
for _, transfer := range cfg.Transfers[chainSelector] {
tx, err := transferOrBuildTx(e, linkState, transfer, opts, chain, cfg.McmsConfig)
if err != nil {
return deployment.ChangesetOutput{}, err
}
op := mcms.Operation{
To: linkAddress,
Data: tx.Data(),
Value: big.NewInt(0),
ContractType: string(types.LinkToken),
}
batch.Batch = append(batch.Batch, op)
totalAmount.Add(totalAmount, transfer.Value)
}

allBatches = append(allBatches, batch)
}

if cfg.McmsConfig != nil {
proposal, err := proposalutils.BuildProposalFromBatches(
timelockAddresses,
mcmsPerChain,
allBatches,
"LINK Value transfer proposal",
cfg.McmsConfig.MinDelay,
)
if err != nil {
return deployment.ChangesetOutput{}, err
}

return deployment.ChangesetOutput{
Proposals: []timelock.MCMSWithTimelockProposal{*proposal},
}, nil
}

return deployment.ChangesetOutput{}, nil
}
Loading

0 comments on commit 97b0563

Please sign in to comment.