diff --git a/deployment/ccip/changeset/cs_add_lane.go b/deployment/ccip/changeset/cs_add_lane.go deleted file mode 100644 index 0bd03b56559..00000000000 --- a/deployment/ccip/changeset/cs_add_lane.go +++ /dev/null @@ -1,227 +0,0 @@ -package changeset - -import ( - "encoding/hex" - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" - - "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/onramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" -) - -var _ deployment.ChangeSet[AddLanesConfig] = AddLanes - -type InitialPrices struct { - LinkPrice *big.Int // USD to the power of 18 (e18) per LINK - WethPrice *big.Int // USD to the power of 18 (e18) per WETH - GasPrice *big.Int // uint224 packed gas price in USD (112 for exec // 112 for da) -} - -func (p InitialPrices) Validate() error { - if p.LinkPrice == nil { - return fmt.Errorf("missing link price") - } - if p.WethPrice == nil { - return fmt.Errorf("missing weth price") - } - if p.GasPrice == nil { - return fmt.Errorf("missing gas price") - } - return nil -} - -type LaneConfig struct { - SourceSelector uint64 - DestSelector uint64 - InitialPricesBySource InitialPrices - FeeQuoterDestChain fee_quoter.FeeQuoterDestChainConfig - TestRouter bool -} - -type AddLanesConfig struct { - LaneConfigs []LaneConfig -} - -func (c AddLanesConfig) Validate() error { - for _, pair := range c.LaneConfigs { - if pair.SourceSelector == pair.DestSelector { - return fmt.Errorf("cannot add lane to the same chain") - } - if err := pair.InitialPricesBySource.Validate(); err != nil { - return fmt.Errorf("error in validating initial prices for chain %d : %w", pair.SourceSelector, err) - } - // TODO: add more FeeQuoterDestChainConfigArgs validation - if pair.FeeQuoterDestChain == (fee_quoter.FeeQuoterDestChainConfig{}) { - return fmt.Errorf("missing fee quoter dest chain config") - } - } - return nil -} - -// AddLanes adds lanes between chains. -// AddLanes is run while the contracts are still owned by the deployer. -// This is useful to test the initial deployment to enable lanes between chains. -// If the testrouter is enabled, the lanes can be used to send messages between chains with testrouter. -// On successful verification with testrouter, the lanes can be enabled with the main router with different addLane ChangeSet. -func AddLanes(e deployment.Environment, cfg AddLanesConfig) (deployment.ChangesetOutput, error) { - if err := cfg.Validate(); err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("invalid AddLanesConfig: %w", err) - } - newAddresses := deployment.NewMemoryAddressBook() - err := addLanes(e, cfg) - if err != nil { - e.Logger.Errorw("Failed to add lanes", "err", err) - return deployment.ChangesetOutput{}, err - } - return deployment.ChangesetOutput{ - Proposals: []timelock.MCMSWithTimelockProposal{}, - AddressBook: newAddresses, - JobSpecs: nil, - }, nil -} - -var DefaultInitialPrices = InitialPrices{ - LinkPrice: deployment.E18Mult(20), - WethPrice: deployment.E18Mult(4000), - GasPrice: ToPackedFee(big.NewInt(8e14), big.NewInt(0)), -} - -func addLanes(e deployment.Environment, cfg AddLanesConfig) error { - state, err := LoadOnchainState(e) - if err != nil { - return fmt.Errorf("failed to load onchain state: %w", err) - } - for _, laneCfg := range cfg.LaneConfigs { - e.Logger.Infow("Enabling lane with test router", "from", laneCfg.SourceSelector, "to", laneCfg.DestSelector) - if err := addLane(e, state, laneCfg, laneCfg.TestRouter); err != nil { - return err - } - } - return nil -} - -func addLane(e deployment.Environment, state CCIPOnChainState, config LaneConfig, isTestRouter bool) error { - // TODO: Batch - var fromRouter *router.Router - var toRouter *router.Router - from := config.SourceSelector - to := config.DestSelector - feeQuoterDestChainConfig := config.FeeQuoterDestChain - initialPrices := config.InitialPricesBySource - if isTestRouter { - fromRouter = state.Chains[from].TestRouter - toRouter = state.Chains[to].TestRouter - } else { - fromRouter = state.Chains[from].Router - toRouter = state.Chains[to].Router - } - tx, err := fromRouter.ApplyRampUpdates(e.Chains[from].DeployerKey, []router.RouterOnRamp{ - { - DestChainSelector: to, - OnRamp: state.Chains[from].OnRamp.Address(), - }, - }, []router.RouterOffRamp{}, []router.RouterOffRamp{}) - if _, err := deployment.ConfirmIfNoError(e.Chains[from], tx, err); err != nil { - return err - } - tx, err = state.Chains[from].OnRamp.ApplyDestChainConfigUpdates(e.Chains[from].DeployerKey, - []onramp.OnRampDestChainConfigArgs{ - { - DestChainSelector: to, - Router: fromRouter.Address(), - }, - }) - if _, err := deployment.ConfirmIfNoError(e.Chains[from], tx, err); err != nil { - return err - } - - _, err = state.Chains[from].FeeQuoter.UpdatePrices( - e.Chains[from].DeployerKey, fee_quoter.InternalPriceUpdates{ - TokenPriceUpdates: []fee_quoter.InternalTokenPriceUpdate{ - { - SourceToken: state.Chains[from].LinkToken.Address(), - UsdPerToken: initialPrices.LinkPrice, - }, - { - SourceToken: state.Chains[from].Weth9.Address(), - UsdPerToken: initialPrices.WethPrice, - }, - }, - GasPriceUpdates: []fee_quoter.InternalGasPriceUpdate{ - { - DestChainSelector: to, - UsdPerUnitGas: initialPrices.GasPrice, - }, - }}) - if _, err := deployment.ConfirmIfNoError(e.Chains[from], tx, err); err != nil { - return err - } - - // Enable dest in fee quoter - tx, err = state.Chains[from].FeeQuoter.ApplyDestChainConfigUpdates(e.Chains[from].DeployerKey, - []fee_quoter.FeeQuoterDestChainConfigArgs{ - { - DestChainSelector: to, - DestChainConfig: feeQuoterDestChainConfig, - }, - }) - if _, err := deployment.ConfirmIfNoError(e.Chains[from], tx, err); err != nil { - return err - } - - tx, err = state.Chains[to].OffRamp.ApplySourceChainConfigUpdates(e.Chains[to].DeployerKey, - []offramp.OffRampSourceChainConfigArgs{ - { - Router: toRouter.Address(), - SourceChainSelector: from, - IsEnabled: true, - OnRamp: common.LeftPadBytes(state.Chains[from].OnRamp.Address().Bytes(), 32), - }, - }) - if _, err := deployment.ConfirmIfNoError(e.Chains[to], tx, err); err != nil { - return err - } - tx, err = toRouter.ApplyRampUpdates(e.Chains[to].DeployerKey, []router.RouterOnRamp{}, []router.RouterOffRamp{}, []router.RouterOffRamp{ - { - SourceChainSelector: from, - OffRamp: state.Chains[to].OffRamp.Address(), - }, - }) - _, err = deployment.ConfirmIfNoError(e.Chains[to], tx, err) - return err -} - -func DefaultFeeQuoterDestChainConfig() fee_quoter.FeeQuoterDestChainConfig { - // https://github.com/smartcontractkit/ccip/blob/c4856b64bd766f1ddbaf5d13b42d3c4b12efde3a/contracts/src/v0.8/ccip/libraries/Internal.sol#L337-L337 - /* - ```Solidity - // bytes4(keccak256("CCIP ChainFamilySelector EVM")) - bytes4 public constant CHAIN_FAMILY_SELECTOR_EVM = 0x2812d52c; - ``` - */ - evmFamilySelector, _ := hex.DecodeString("2812d52c") - return fee_quoter.FeeQuoterDestChainConfig{ - IsEnabled: true, - MaxNumberOfTokensPerMsg: 10, - MaxDataBytes: 256, - MaxPerMsgGasLimit: 3_000_000, - DestGasOverhead: ccipevm.DestGasOverhead, - DefaultTokenFeeUSDCents: 1, - DestGasPerPayloadByte: ccipevm.CalldataGasPerByte, - DestDataAvailabilityOverheadGas: 100, - DestGasPerDataAvailabilityByte: 100, - DestDataAvailabilityMultiplierBps: 1, - DefaultTokenDestGasOverhead: 125_000, - DefaultTxGasLimit: 200_000, - GasMultiplierWeiPerEth: 11e17, // Gas multiplier in wei per eth is scaled by 1e18, so 11e17 is 1.1 = 110% - NetworkFeeUSDCents: 1, - ChainFamilySelector: [4]byte(evmFamilySelector), - } -} diff --git a/deployment/ccip/changeset/cs_add_lane_test.go b/deployment/ccip/changeset/cs_add_lane_test.go index 5c324c975ef..0870ede2e55 100644 --- a/deployment/ccip/changeset/cs_add_lane_test.go +++ b/deployment/ccip/changeset/cs_add_lane_test.go @@ -1,20 +1,26 @@ package changeset import ( + "math/big" "testing" - "time" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" - commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" + commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" ) +var ( + LINKPrice = deployment.E18Mult(20) + WETHPrice = deployment.E18Mult(4000) + GasPrice = ToPackedFee(big.NewInt(8e14), big.NewInt(0)) +) + func TestAddLanesWithTestRouter(t *testing.T) { t.Parallel() e := NewMemoryEnvironment(t) @@ -25,14 +31,79 @@ func TestAddLanesWithTestRouter(t *testing.T) { selectors := e.Env.AllChainSelectors() chain1, chain2 := selectors[0], selectors[1] - _, err = AddLanes(e.Env, AddLanesConfig{ - LaneConfigs: []LaneConfig{ - { - SourceSelector: chain1, - DestSelector: chain2, - InitialPricesBySource: DefaultInitialPrices, - FeeQuoterDestChain: DefaultFeeQuoterDestChainConfig(), - TestRouter: true, + stateChain1 := state.Chains[chain1] + e.Env, err = commoncs.ApplyChangesets(t, e.Env, e.TimelockContracts(t), []commoncs.ChangesetApplication{ + { + Changeset: commoncs.WrapChangeSet(UpdateOnRampsDests), + Config: UpdateOnRampDestsConfig{ + UpdatesByChain: map[uint64]map[uint64]OnRampDestinationUpdate{ + chain1: { + chain2: { + IsEnabled: true, + TestRouter: true, + AllowListEnabled: false, + }, + }, + }, + }, + }, + { + Changeset: commoncs.WrapChangeSet(UpdateFeeQuoterPricesCS), + Config: UpdateFeeQuoterPricesConfig{ + InitialPrices: map[uint64]FeeQuoterPriceUpdatePerSource{ + chain1: { + TokenPrices: map[common.Address]*big.Int{ + stateChain1.LinkToken.Address(): LINKPrice, + stateChain1.Weth9.Address(): WETHPrice, + }, + GasPrices: map[uint64]*big.Int{ + chain2: GasPrice, + }, + }, + }, + }, + }, + { + Changeset: commoncs.WrapChangeSet(UpdateFeeQuoterDests), + Config: UpdateFeeQuoterDestsConfig{ + UpdatesByChain: map[uint64]map[uint64]fee_quoter.FeeQuoterDestChainConfig{ + chain1: { + chain2: DefaultFeeQuoterDestChainConfig(), + }, + }, + }, + }, + { + Changeset: commoncs.WrapChangeSet(UpdateOffRampSources), + Config: UpdateOffRampSourcesConfig{ + UpdatesByChain: map[uint64]map[uint64]OffRampSourceUpdate{ + chain2: { + chain1: { + IsEnabled: true, + TestRouter: true, + }, + }, + }, + }, + }, + { + Changeset: commoncs.WrapChangeSet(UpdateRouterRamps), + Config: UpdateRouterRampsConfig{ + TestRouter: true, + UpdatesByChain: map[uint64]RouterUpdates{ + // onRamp update on source chain + chain1: { + OnRampUpdates: map[uint64]bool{ + chain2: true, + }, + }, + // off + chain2: { + OffRampUpdates: map[uint64]bool{ + chain1: true, + }, + }, + }, }, }, }) @@ -58,142 +129,3 @@ func TestAddLanesWithTestRouter(t *testing.T) { }] = []uint64{msgSentEvent.SequenceNumber} ConfirmExecWithSeqNrsForAll(t, e.Env, state, expectedSeqNumExec, startBlocks) } - -// TestAddLane covers the workflow of adding a lane between two chains and enabling it. -// It also covers the case where the onRamp is disabled on the OffRamp contract initially and then enabled. -func TestAddLane(t *testing.T) { - t.Skip("This test is flaky and needs to be fixed: reverted," + - "error reason: 0x07da6ee6 InsufficientFeeTokenAmount: Replace time.sleep() with polling") - - t.Parallel() - // We add more chains to the chainlink nodes than the number of chains where CCIP is deployed. - e := NewMemoryEnvironment(t) - // Here we have CR + nodes set up, but no CCIP contracts deployed. - state, err := LoadOnchainState(e.Env) - require.NoError(t, err) - - selectors := e.Env.AllChainSelectors() - chain1, chain2 := selectors[0], selectors[1] - - // We expect no lanes available on any chain. - for _, sel := range []uint64{chain1, chain2} { - chain := state.Chains[sel] - offRamps, err := chain.Router.GetOffRamps(nil) - require.NoError(t, err) - require.Len(t, offRamps, 0) - } - - replayBlocks, err := LatestBlocksByChain(testcontext.Get(t), e.Env.Chains) - require.NoError(t, err) - - // Add one lane from chain1 to chain 2 and send traffic. - require.NoError(t, AddLaneWithDefaultPricesAndFeeQuoterConfig(e.Env, state, chain1, chain2, false)) - - ReplayLogs(t, e.Env.Offchain, replayBlocks) - time.Sleep(30 * time.Second) - // disable the onRamp initially on OffRamp - disableRampTx, err := state.Chains[chain2].OffRamp.ApplySourceChainConfigUpdates(e.Env.Chains[chain2].DeployerKey, []offramp.OffRampSourceChainConfigArgs{ - { - Router: state.Chains[chain2].Router.Address(), - SourceChainSelector: chain1, - IsEnabled: false, - OnRamp: common.LeftPadBytes(state.Chains[chain1].OnRamp.Address().Bytes(), 32), - }, - }) - _, err = deployment.ConfirmIfNoError(e.Env.Chains[chain2], disableRampTx, err) - require.NoError(t, err) - - for _, sel := range []uint64{chain1, chain2} { - chain := state.Chains[sel] - offRamps, err := chain.Router.GetOffRamps(nil) - require.NoError(t, err) - if sel == chain2 { - require.Len(t, offRamps, 1) - srcCfg, err := chain.OffRamp.GetSourceChainConfig(nil, chain1) - require.NoError(t, err) - require.Equal(t, common.LeftPadBytes(state.Chains[chain1].OnRamp.Address().Bytes(), 32), srcCfg.OnRamp) - require.False(t, srcCfg.IsEnabled) - } else { - require.Len(t, offRamps, 0) - } - } - - latesthdr, err := e.Env.Chains[chain2].Client.HeaderByNumber(testcontext.Get(t), nil) - require.NoError(t, err) - startBlock := latesthdr.Number.Uint64() - // Send traffic on the first lane and it should not be processed by the plugin as onRamp is disabled - // we will check this by confirming that the message is not executed by the end of the test - msgSentEvent1 := TestSendRequest(t, e.Env, state, chain1, chain2, false, router.ClientEVM2AnyMessage{ - Receiver: common.LeftPadBytes(state.Chains[chain2].Receiver.Address().Bytes(), 32), - Data: []byte("hello world"), - TokenAmounts: nil, - FeeToken: common.HexToAddress("0x0"), - ExtraArgs: nil, - }) - require.Equal(t, uint64(1), msgSentEvent1.SequenceNumber) - - // Add another lane - require.NoError(t, AddLaneWithDefaultPricesAndFeeQuoterConfig(e.Env, state, chain2, chain1, false)) - - // Send traffic on the second lane and it should succeed - latesthdr, err = e.Env.Chains[chain1].Client.HeaderByNumber(testcontext.Get(t), nil) - require.NoError(t, err) - startBlock2 := latesthdr.Number.Uint64() - msgSentEvent2 := TestSendRequest(t, e.Env, state, chain2, chain1, false, router.ClientEVM2AnyMessage{ - Receiver: common.LeftPadBytes(state.Chains[chain2].Receiver.Address().Bytes(), 32), - Data: []byte("hello world"), - TokenAmounts: nil, - FeeToken: common.HexToAddress("0x0"), - ExtraArgs: nil, - }) - require.Equal(t, uint64(1), msgSentEvent2.SequenceNumber) - require.NoError(t, - commonutils.JustError( - ConfirmExecWithSeqNrs( - t, - e.Env.Chains[chain2], - e.Env.Chains[chain1], - state.Chains[chain1].OffRamp, - &startBlock2, - []uint64{msgSentEvent2.SequenceNumber}, - ), - ), - ) - - // now check for the previous message from chain 1 to chain 2 that it has not been executed till now as the onRamp was disabled - ConfirmNoExecConsistentlyWithSeqNr(t, e.Env.Chains[chain1], e.Env.Chains[chain2], state.Chains[chain2].OffRamp, msgSentEvent1.SequenceNumber, 30*time.Second) - - // enable the onRamp on OffRamp - enableRampTx, err := state.Chains[chain2].OffRamp.ApplySourceChainConfigUpdates(e.Env.Chains[chain2].DeployerKey, []offramp.OffRampSourceChainConfigArgs{ - { - Router: state.Chains[chain2].Router.Address(), - SourceChainSelector: chain1, - IsEnabled: true, - OnRamp: common.LeftPadBytes(state.Chains[chain1].OnRamp.Address().Bytes(), 32), - }, - }) - _, err = deployment.ConfirmIfNoError(e.Env.Chains[chain2], enableRampTx, err) - require.NoError(t, err) - - srcCfg, err := state.Chains[chain2].OffRamp.GetSourceChainConfig(nil, chain1) - require.NoError(t, err) - require.Equal(t, common.LeftPadBytes(state.Chains[chain1].OnRamp.Address().Bytes(), 32), srcCfg.OnRamp) - require.True(t, srcCfg.IsEnabled) - - // we need the replay here otherwise plugin is not able to locate the message - ReplayLogs(t, e.Env.Offchain, replayBlocks) - time.Sleep(30 * time.Second) - // Now that the onRamp is enabled, the request should be processed - require.NoError(t, - commonutils.JustError( - ConfirmExecWithSeqNrs( - t, - e.Env.Chains[chain1], - e.Env.Chains[chain2], - state.Chains[chain2].OffRamp, - &startBlock, - []uint64{msgSentEvent1.SequenceNumber}, - ), - ), - ) -} diff --git a/deployment/ccip/changeset/cs_chain_contracts.go b/deployment/ccip/changeset/cs_chain_contracts.go index e97772793b0..6f04d048ed2 100644 --- a/deployment/ccip/changeset/cs_chain_contracts.go +++ b/deployment/ccip/changeset/cs_chain_contracts.go @@ -4,35 +4,167 @@ import ( "bytes" "context" "encoding/hex" + "errors" "fmt" "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "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-common/pkg/logger" + "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm" cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/nonce_manager" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" ) var ( - _ deployment.ChangeSet[UpdateOnRampDestsConfig] = UpdateOnRampsDests - _ deployment.ChangeSet[UpdateOffRampSourcesConfig] = UpdateOffRampSources - _ deployment.ChangeSet[UpdateRouterRampsConfig] = UpdateRouterRamps - _ deployment.ChangeSet[UpdateFeeQuoterDestsConfig] = UpdateFeeQuoterDests - _ deployment.ChangeSet[SetOCR3OffRampConfig] = SetOCR3OffRamp + _ deployment.ChangeSet[UpdateOnRampDestsConfig] = UpdateOnRampsDests + _ deployment.ChangeSet[UpdateOffRampSourcesConfig] = UpdateOffRampSources + _ deployment.ChangeSet[UpdateRouterRampsConfig] = UpdateRouterRamps + _ deployment.ChangeSet[UpdateFeeQuoterDestsConfig] = UpdateFeeQuoterDests + _ deployment.ChangeSet[SetOCR3OffRampConfig] = SetOCR3OffRamp + _ deployment.ChangeSet[UpdateFeeQuoterPricesConfig] = UpdateFeeQuoterPricesCS + _ deployment.ChangeSet[UpdateNonceManagerConfig] = UpdateNonceManagersCS ) +type UpdateNonceManagerConfig struct { + UpdatesByChain map[uint64]NonceManagerUpdate // source -> dest -> update + MCMS *MCMSConfig +} + +type NonceManagerUpdate struct { + AddedAuthCallers []common.Address + RemovedAuthCallers []common.Address + PreviousRampsArgs []nonce_manager.NonceManagerPreviousRampsArgs +} + +func (cfg UpdateNonceManagerConfig) Validate(e deployment.Environment) error { + state, err := LoadOnchainState(e) + if err != nil { + return err + } + for sourceSel := range cfg.UpdatesByChain { + sourceChainState, ok := state.Chains[sourceSel] + if !ok { + return fmt.Errorf("chain %d not found in onchain state", sourceSel) + } + if sourceChainState.NonceManager == nil { + return fmt.Errorf("missing nonce manager for chain %d", sourceSel) + } + sourceChain, ok := e.Chains[sourceSel] + if !ok { + return fmt.Errorf("missing chain %d in environment", sourceSel) + } + if err := commoncs.ValidateOwnership(e.GetContext(), cfg.MCMS != nil, sourceChain.DeployerKey.From, sourceChainState.Timelock.Address(), sourceChainState.OnRamp); err != nil { + return fmt.Errorf("chain %s: %w", sourceChain.String(), err) + } + } + return nil +} + +func UpdateNonceManagersCS(e deployment.Environment, cfg UpdateNonceManagerConfig) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, err + } + s, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + var batches []timelock.BatchChainOperation + timelocks := make(map[uint64]common.Address) + proposers := make(map[uint64]*gethwrappers.ManyChainMultiSig) + for chainSel, updates := range cfg.UpdatesByChain { + txOpts := e.Chains[chainSel].DeployerKey + if cfg.MCMS != nil { + txOpts = deployment.SimTransactOpts() + } + nm := s.Chains[chainSel].NonceManager + var authTx, prevRampsTx *types.Transaction + if len(updates.AddedAuthCallers) > 0 || len(updates.RemovedAuthCallers) > 0 { + authTx, err = nm.ApplyAuthorizedCallerUpdates(txOpts, nonce_manager.AuthorizedCallersAuthorizedCallerArgs{ + AddedCallers: updates.AddedAuthCallers, + RemovedCallers: updates.RemovedAuthCallers, + }) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("error updating authorized callers for chain %s: %w", e.Chains[chainSel].String(), err) + } + } + if len(updates.PreviousRampsArgs) > 0 { + prevRampsTx, err = nm.ApplyPreviousRampsUpdates(txOpts, updates.PreviousRampsArgs) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("error updating previous ramps for chain %s: %w", e.Chains[chainSel].String(), err) + } + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("error updating previous ramps for chain %s: %w", e.Chains[chainSel].String(), err) + } + } + if cfg.MCMS == nil { + if authTx != nil { + if _, err := deployment.ConfirmIfNoError(e.Chains[chainSel], authTx, err); err != nil { + return deployment.ChangesetOutput{}, err + } + } + if prevRampsTx != nil { + if _, err := deployment.ConfirmIfNoError(e.Chains[chainSel], prevRampsTx, err); err != nil { + return deployment.ChangesetOutput{}, err + } + } + } else { + ops := make([]mcms.Operation, 0) + if authTx != nil { + ops = append(ops, mcms.Operation{ + To: nm.Address(), + Data: authTx.Data(), + Value: big.NewInt(0), + }) + } + if prevRampsTx != nil { + ops = append(ops, mcms.Operation{ + To: nm.Address(), + Data: prevRampsTx.Data(), + Value: big.NewInt(0), + }) + } + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(chainSel), + Batch: ops, + }) + timelocks[chainSel] = s.Chains[chainSel].Timelock.Address() + proposers[chainSel] = s.Chains[chainSel].ProposerMcm + } + } + if cfg.MCMS == nil { + return deployment.ChangesetOutput{}, nil + } + + p, err := proposalutils.BuildProposalFromBatches( + timelocks, + proposers, + batches, + "Update nonce manager for previous ramps and authorized callers", + cfg.MCMS.MinDelay, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + return deployment.ChangesetOutput{Proposals: []timelock.MCMSWithTimelockProposal{ + *p, + }}, nil +} + type UpdateOnRampDestsConfig struct { UpdatesByChain map[uint64]map[uint64]OnRampDestinationUpdate // Disallow mixing MCMS/non-MCMS per chain for simplicity. @@ -167,6 +299,143 @@ func UpdateOnRampsDests(e deployment.Environment, cfg UpdateOnRampDestsConfig) ( }}, nil } +type UpdateFeeQuoterPricesConfig struct { + InitialPrices map[uint64]FeeQuoterPriceUpdatePerSource + MCMS *MCMSConfig +} + +type FeeQuoterPriceUpdatePerSource struct { + TokenPrices map[common.Address]*big.Int // token address -> price + GasPrices map[uint64]*big.Int // dest chain -> gas price +} + +func (cfg UpdateFeeQuoterPricesConfig) Validate(e deployment.Environment) error { + state, err := LoadOnchainState(e) + if err != nil { + return err + } + for chainSel, initialPrice := range cfg.InitialPrices { + if err := deployment.IsValidChainSelector(chainSel); err != nil { + return fmt.Errorf("invalid chain selector: %w", err) + } + chainState, ok := state.Chains[chainSel] + if !ok { + return fmt.Errorf("chain %d not found in onchain state", chainSel) + } + if chainState.FeeQuoter == nil { + return fmt.Errorf("missing fee quoter for chain %d", chainSel) + } + if err := commoncs.ValidateOwnership(e.GetContext(), cfg.MCMS != nil, e.Chains[chainSel].DeployerKey.From, chainState.Timelock.Address(), chainState.FeeQuoter); err != nil { + return err + } + for token, price := range initialPrice.TokenPrices { + if price == nil { + return fmt.Errorf("token price for chain %d is nil", chainSel) + } + if token == (common.Address{}) { + return fmt.Errorf("token address for chain %d is empty", chainSel) + } + contains, err := deployment.AddressBookContains(e.ExistingAddresses, chainSel, token.String()) + if err != nil { + return fmt.Errorf("error checking address book for token %s: %w", token.String(), err) + } + if !contains { + return fmt.Errorf("token %s not found in address book for chain %d", token.String(), chainSel) + } + } + for dest, price := range initialPrice.GasPrices { + if chainSel == dest { + return errors.New("source and dest chain cannot be the same") + } + if err := deployment.IsValidChainSelector(dest); err != nil { + return fmt.Errorf("invalid dest chain selector: %w", err) + } + if price == nil { + return fmt.Errorf("gas price for chain %d is nil", chainSel) + } + if _, ok := state.Chains[dest]; !ok { + return fmt.Errorf("dest chain %d not found in onchain state for chain %d", dest, chainSel) + } + } + } + + return nil +} + +func UpdateFeeQuoterPricesCS(e deployment.Environment, cfg UpdateFeeQuoterPricesConfig) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, err + } + s, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + var batches []timelock.BatchChainOperation + timelocks := make(map[uint64]common.Address) + proposers := make(map[uint64]*gethwrappers.ManyChainMultiSig) + for chainSel, initialPrice := range cfg.InitialPrices { + txOpts := e.Chains[chainSel].DeployerKey + if cfg.MCMS != nil { + txOpts = deployment.SimTransactOpts() + } + fq := s.Chains[chainSel].FeeQuoter + var tokenPricesArgs []fee_quoter.InternalTokenPriceUpdate + for token, price := range initialPrice.TokenPrices { + tokenPricesArgs = append(tokenPricesArgs, fee_quoter.InternalTokenPriceUpdate{ + SourceToken: token, + UsdPerToken: price, + }) + } + var gasPricesArgs []fee_quoter.InternalGasPriceUpdate + for dest, price := range initialPrice.GasPrices { + gasPricesArgs = append(gasPricesArgs, fee_quoter.InternalGasPriceUpdate{ + DestChainSelector: dest, + UsdPerUnitGas: price, + }) + } + tx, err := fq.UpdatePrices(txOpts, fee_quoter.InternalPriceUpdates{ + TokenPriceUpdates: tokenPricesArgs, + GasPriceUpdates: gasPricesArgs, + }) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("error updating prices for chain %s: %w", e.Chains[chainSel].String(), err) + } + if cfg.MCMS == nil { + if _, err := deployment.ConfirmIfNoError(e.Chains[chainSel], tx, err); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("error confirming transaction for chain %s: %w", e.Chains[chainSel].String(), err) + } + } else { + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(chainSel), + Batch: []mcms.Operation{ + { + To: fq.Address(), + Data: tx.Data(), + Value: big.NewInt(0), + }, + }, + }) + } + } + if cfg.MCMS == nil { + return deployment.ChangesetOutput{}, nil + } + + p, err := proposalutils.BuildProposalFromBatches( + timelocks, + proposers, + batches, + "Update fq prices", + cfg.MCMS.MinDelay, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + return deployment.ChangesetOutput{Proposals: []timelock.MCMSWithTimelockProposal{ + *p, + }}, nil +} + type UpdateFeeQuoterDestsConfig struct { UpdatesByChain map[uint64]map[uint64]fee_quoter.FeeQuoterDestChainConfig // Disallow mixing MCMS/non-MCMS per chain for simplicity. @@ -208,7 +477,7 @@ func (cfg UpdateFeeQuoterDestsConfig) Validate(e deployment.Environment) error { return fmt.Errorf("failed to get onramp static config %s: %w", chainState.OnRamp.Address(), err) } if destination == sc.ChainSelector { - return fmt.Errorf("cannot update onramp destination to the same chain") + return fmt.Errorf("source and destination chain cannot be the same") } } } @@ -755,3 +1024,31 @@ func isOCR3ConfigSetOnOffRamp( } return true, nil } + +func DefaultFeeQuoterDestChainConfig() fee_quoter.FeeQuoterDestChainConfig { + // https://github.com/smartcontractkit/ccip/blob/c4856b64bd766f1ddbaf5d13b42d3c4b12efde3a/contracts/src/v0.8/ccip/libraries/Internal.sol#L337-L337 + /* + ```Solidity + // bytes4(keccak256("CCIP ChainFamilySelector EVM")) + bytes4 public constant CHAIN_FAMILY_SELECTOR_EVM = 0x2812d52c; + ``` + */ + evmFamilySelector, _ := hex.DecodeString("2812d52c") + return fee_quoter.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxNumberOfTokensPerMsg: 10, + MaxDataBytes: 256, + MaxPerMsgGasLimit: 3_000_000, + DestGasOverhead: ccipevm.DestGasOverhead, + DefaultTokenFeeUSDCents: 1, + DestGasPerPayloadByte: ccipevm.CalldataGasPerByte, + DestDataAvailabilityOverheadGas: 100, + DestGasPerDataAvailabilityByte: 100, + DestDataAvailabilityMultiplierBps: 1, + DefaultTokenDestGasOverhead: 125_000, + DefaultTxGasLimit: 200_000, + GasMultiplierWeiPerEth: 11e17, // Gas multiplier in wei per eth is scaled by 1e18, so 11e17 is 1.1 = 110% + NetworkFeeUSDCents: 1, + ChainFamilySelector: [4]byte(evmFamilySelector), + } +}