-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: link transfer mcms changesets (#15512)
* 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
1 parent
7ca4930
commit 97b0563
Showing
16 changed files
with
955 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
deployment/common/changeset/example/add_mint_burners_link.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
50
deployment/common/changeset/example/add_mint_burners_link_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.