Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: link transfer mcms changesets #15512

Merged
merged 47 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a6910ae
feat: link transfer with timelock changeset
ecPablo Dec 3, 2024
c46c5d4
feat: link transfer and approval integration tests and changesets.
ecPablo Dec 4, 2024
869cf6a
feat: rename files
ecPablo Dec 4, 2024
c0f3969
fix: use deployment.SimTransactOpts() to get tx data
ecPablo Dec 4, 2024
239d7b7
fix: link contract creation
ecPablo Dec 5, 2024
9060af1
fix: remove approval changeset, not necessary for sending directly fr…
ecPablo Dec 5, 2024
a1eaa94
feat: make config accept a map of chain selectors for the proposal ge…
ecPablo Dec 5, 2024
9897c40
Merge branch 'refs/heads/develop' into ecPablo/link-transfer-mcms
ecPablo Dec 5, 2024
f9fd24c
fix: params on deploy link
ecPablo Dec 5, 2024
b5d343a
fix: simplify config args by using state helper functions.
ecPablo Dec 5, 2024
c54218f
fix: use pointer for value
ecPablo Dec 5, 2024
9dca3be
feat: add mint permissions and minting link changeset
ecPablo Dec 6, 2024
4d007c3
Deploy call proxy instead of using deployer executor keys
akhilchainani Dec 6, 2024
101b9fd
inject call proxies in execution methods
akhilchainani Dec 8, 2024
663f8b0
skip call proxy when loading chain state
akhilchainani Dec 8, 2024
c17911e
revert all changes
akhilchainani Dec 8, 2024
5b96629
Revert "revert all changes"
akhilchainani Dec 8, 2024
aa75a47
Merge branch 'refs/heads/fix/deployment-call-proxy' into ecPablo/link…
ecPablo Dec 9, 2024
b6cc93f
Merge branch 'refs/heads/develop' into ecPablo/link-transfer-mcms
ecPablo Dec 9, 2024
a51f09a
chore: rename load state funcs
ecPablo Dec 9, 2024
898a20d
feat: add mcms config flag
ecPablo Dec 10, 2024
7d2203c
Merge branch 'refs/heads/develop' into ecPablo/link-transfer-mcms
ecPablo Dec 10, 2024
96d2a8f
fix: integration tests after merging develop
ecPablo Dec 10, 2024
c3e528c
fix: use contracts from states and code improvements
ecPablo Dec 10, 2024
127aa4d
fix: cs deploy chain args
ecPablo Dec 10, 2024
c898322
fix: params ccip boosting
ecPablo Dec 10, 2024
ba6b57d
fix: bundle mcms config into single struct
ecPablo Dec 10, 2024
e6ac21a
fix: add more validations for config
ecPablo Dec 10, 2024
001b002
fix: remove startingOpCount and use proposal utils to derive it
ecPablo Dec 10, 2024
6641f4f
fix: adjust variable names, remove boolean for mcms config, add const…
ecPablo Dec 11, 2024
f889dfc
feat: add tests for non mcms case, improve validations, and wait for …
ecPablo Dec 11, 2024
b9479fc
feat: check valid until is in future
ecPablo Dec 11, 2024
03e9382
feat: add tests for Validate() and small validation fixes
ecPablo Dec 11, 2024
3d2dad1
fix: rename MaybeLoadLinkTokenState to MaybeLoadLinkTokenChainState t…
ecPablo Dec 11, 2024
911130a
Update deployment/common/changeset/example/link_transfer.go
ecPablo Dec 11, 2024
7dd0fdd
fix: error handling and validations
ecPablo Dec 11, 2024
9bc7467
fix: use getDeployer helper
ecPablo Dec 11, 2024
c8f9036
feat: split mint burners into a separate changeset
ecPablo Dec 11, 2024
8a25dbc
fix: name TestMintLink on unit test
ecPablo Dec 11, 2024
fd9fab4
Update deployment/common/changeset/example/add_mint_burners_link.go
ecPablo Dec 12, 2024
4c68ec8
Update deployment/common/changeset/example/add_mint_burners_link.go
ecPablo Dec 12, 2024
9d855b5
Merge branch 'refs/heads/develop' into ecPablo/link-transfer-mcms
ecPablo Dec 12, 2024
9412602
fix: use changeset apply for unit tests environment setup
ecPablo Dec 12, 2024
6fd93b9
fix: linting errors
ecPablo Dec 12, 2024
f6596c4
Merge branch 'refs/heads/develop' into ecPablo/link-transfer-mcms
ecPablo Dec 12, 2024
f0aa7da
fix: merge conflicts
ecPablo Dec 12, 2024
7c2ecf0
fix: remove valid unit to reuse util for proposal creation
ecPablo Dec 13, 2024
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
4 changes: 2 additions & 2 deletions deployment/ccip/changeset/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,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)
ecPablo marked this conversation as resolved.
Show resolved Hide resolved
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

}
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)
ecPablo marked this conversation as resolved.
Show resolved Hide resolved
}
chain, ok := e.Chains[chainSel]
if !ok {
return fmt.Errorf("chain with selector %d not found", chainSel)
}
addrs, err := e.ExistingAddresses.AddressesForChain(chainSel)
ecPablo marked this conversation as resolved.
Show resolved Hide resolved
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)
ecPablo marked this conversation as resolved.
Show resolved Hide resolved
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{
ecPablo marked this conversation as resolved.
Show resolved Hide resolved
To: linkAddress,
Data: tx.Data(),
Value: big.NewInt(0),
ContractType: string(types.LinkToken),
}
batch.Batch = append(batch.Batch, op)
totalAmount.Add(totalAmount, transfer.Value)
ecPablo marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading