diff --git a/ccv/chains/evm/deployment/go.mod b/ccv/chains/evm/deployment/go.mod index 88fcf13415..bd9464f010 100644 --- a/ccv/chains/evm/deployment/go.mod +++ b/ccv/chains/evm/deployment/go.mod @@ -5,6 +5,10 @@ go 1.25.5 // Taken from CLDF go.mod: https://github.com/smartcontractkit/chainlink-deployments-framework/blob/main/go.mod replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.4 +replace github.com/smartcontractkit/chainlink-ccip/deployment => ../../../../deployment + +replace github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment => ../../../../chains/evm/deployment + require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/ethereum/go-ethereum v1.16.8 @@ -18,6 +22,7 @@ require ( github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd github.com/smartcontractkit/mcms v0.34.0 github.com/stretchr/testify v1.11.1 + golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc ) require ( @@ -203,10 +208,12 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20250826190403-aed7f5f33cde // indirect github.com/smartcontractkit/chainlink-aptos v0.0.0-20251024142440-51f2ad2652a2 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250908144012-8184001834b5 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect + github.com/smartcontractkit/chainlink-evm v0.3.3 // indirect github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9 // indirect github.com/smartcontractkit/chainlink-protos/job-distributor v0.17.0 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect @@ -265,7 +272,6 @@ require ( go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/zap v1.27.1 // indirect golang.org/x/crypto v0.47.0 // indirect - golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/ccv/chains/evm/deployment/go.sum b/ccv/chains/evm/deployment/go.sum index ca7980037a..a96f16935e 100644 --- a/ccv/chains/evm/deployment/go.sum +++ b/ccv/chains/evm/deployment/go.sum @@ -584,8 +584,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -674,6 +674,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20250826190403-aed7f5f33cde h1:dMUf1YOX5hdUkQDgnA/A/sWVoMaQWGB+EYBPuYffYIA= +github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20250826190403-aed7f5f33cde/go.mod h1:SYc+BvAALmwsx2zMJIAczIyVNwsiXRIBXmejcTORxGE= github.com/smartcontractkit/chain-selectors v1.0.89 h1:L9oWZGqQXWyTPnC6ODXgu3b0DFyLmJ9eHv+uJrE9IZY= github.com/smartcontractkit/chain-selectors v1.0.89/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20251024142440-51f2ad2652a2 h1:vGdeMwHO3ow88HvxfhA4DDPYNY0X9jmdux7L83UF/W8= @@ -682,20 +684,18 @@ github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260206181544-f1613c github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260206181544-f1613c67d071/go.mod h1:ZtZ+wtqU9JsJEmbiCsavVVEbhywpgMF7q/IpD9Eaq48= github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260206181544-f1613c67d071 h1:64bvvq3x6F8sJD57B8fLFvaVTOSieEiTMqod2Nwo/ZU= github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260206181544-f1613c67d071/go.mod h1:Gl35ExaFLinqVhp50+Yq1GnMuHb3fnDtZUFPCtcfV3M= -github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260206181544-f1613c67d071 h1:HbJ2HN7lL/eeYJXVYNpfE9uXW3BJ5r9dwA9Tbdv2AtU= -github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260206181544-f1613c67d071/go.mod h1:35R2jCvDmWdLe38d/23mdJ3D9lb1jzsvkb4g0JsFPCM= github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d h1:xdFpzbApEMz4Rojg2Y2OjFlrh0wu7eB10V2tSZGW5y8= github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d/go.mod h1:bgmqE7x9xwmIVr8PqLbC0M5iPm4AV2DBl596lO6S5Sw= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250908144012-8184001834b5 h1:QhcYGEhRLInr1/qh/3RJiVdvJ0nxBHKhPe65WLbSBnU= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250908144012-8184001834b5/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260206181544-f1613c67d071 h1:6cjdvsXpTRSM6jC/Vsc3SYcbKbVuVDmtkvsAcOXoK6M= -github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260206181544-f1613c67d071/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-common v0.9.6-0.20260114142648-bd9e1b483e96 h1:ZnBBOLyMLJjgQQm7WRJl8sA9Q2RhwagJ+WR62VnA3MY= github.com/smartcontractkit/chainlink-common v0.9.6-0.20260114142648-bd9e1b483e96/go.mod h1:DAwaVSiQMgAsCjHa8nOnIAM9GixuIQWsgEZFGpf3JxE= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10/go.mod h1:oiDa54M0FwxevWwyAX773lwdWvFYYlYHHQV1LQ5HpWY= github.com/smartcontractkit/chainlink-deployments-framework v0.74.2 h1:OP4TAlIz8gz0iokYCBleQlpSoxXUBhqUPyV8Xfr34ek= github.com/smartcontractkit/chainlink-deployments-framework v0.74.2/go.mod h1:YHsMLM5bS9rhvXssmsJtvhmF4tDAm/kdmQi+ws/PLEw= +github.com/smartcontractkit/chainlink-evm v0.3.3 h1:JqwyJEtnNEUaoQQPoOBTT4sn2lpdIZHtf0Hr0M60YDw= +github.com/smartcontractkit/chainlink-evm v0.3.3/go.mod h1:q0ZBvaoisNaqC8NcMYWNPTjee88nQktDEeJMQHq3hVI= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd h1:sK+pK4epQp20yQ7XztwrVgkTkRAr4FY+TvEegW8RuQk= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd/go.mod h1:7Jlt72+V9891y3LnGwHzmQwt9tfEGYryRKiGlQHo/o8= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9 h1:QRWXJusIj/IRY5Pl3JclNvDre0cZPd/5NbILwc4RV2M= diff --git a/ccv/chains/evm/deployment/v1_7_0/adapters/feequoterupdater.go b/ccv/chains/evm/deployment/v1_7_0/adapters/feequoterupdater.go new file mode 100644 index 0000000000..56afbfb643 --- /dev/null +++ b/ccv/chains/evm/deployment/v1_7_0/adapters/feequoterupdater.go @@ -0,0 +1,80 @@ +package adapters + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + sequence1_7 "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/sequences" + "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" +) + +// feeQUpdateArgsFromConcrete converts the concrete v1.7 FeeQuoterUpdate to the type parameter (used as any when registered). +func feeQUpdateArgsFromConcrete[T any](out sequence1_7.FeeQuoterUpdate) T { + return any(out).(T) +} + +// FeeQuoterUpdater uses FeeQUpdateArgs any so it implements deploy.FeeQuoterUpdater[any] and can be registered directly. +// The implementation is for v1.7.0 and uses sequence1_7.FeeQuoterUpdate internally. +type FeeQuoterUpdater[FeeQUpdateArgs any] struct{} + +func (fqu FeeQuoterUpdater[FeeQUpdateArgs]) SequenceFeeQuoterInputCreation() *cldf_ops.Sequence[deploy.FeeQuoterUpdateInput, FeeQUpdateArgs, cldf_chain.BlockChains] { + return cldf_ops.NewSequence( + "fee-quoter-updater:input-creation", + semver.MustParse("1.7.0"), + "Creates FeeQuoterUpdateInput for FeeQuoter update sequence", + func(b cldf_ops.Bundle, chains cldf_chain.BlockChains, input deploy.FeeQuoterUpdateInput) (output FeeQUpdateArgs, err error) { + var zero FeeQUpdateArgs + chain, ok := chains.EVMChains()[input.ChainSelector] + if !ok { + return zero, fmt.Errorf("chain with selector %d not found in environment", input.ChainSelector) + } + // get the FeeQuoterUpdateOutput from both v1.6.0 and v1.5.0 sequences and combine them to create the input for the fee quoter update sequence + report, err := cldf_ops.ExecuteSequence(b, sequence1_7.CreateFeeQuoterUpdateInputFromV163, chain, input) + if err != nil { + return zero, fmt.Errorf("failed to create FeeQuoterUpdateInput from v1.6.0: %w", err) + } + output16 := report.Output + + report15, err := cldf_ops.ExecuteSequence(b, sequence1_7.CreateFeeQuoterUpdateInputFromV150, chain, input) + if err != nil { + return zero, fmt.Errorf("failed to create FeeQuoterUpdateInput from v1.5.0: %w", err) + } + output15 := report15.Output + // combine the outputs from both sequences to create the input for the fee quoter update sequence + out, err := sequence1_7.MergeFeeQuoterUpdateOutputs(output16, output15) + if err != nil { + return zero, fmt.Errorf("failed to merge FeeQuoterUpdateInput from v1.6.0 and v1.5.0: %w", err) + } + // check if output is empty, if so, return an error + if empty, err := out.IsEmpty(); err != nil || empty { + return zero, fmt.Errorf("could not create input for fee quoter 1.7 update sequence: %w", err) + } + out.ChainSelector = input.ChainSelector + out.ExistingAddresses = input.ExistingAddresses + return any(out).(FeeQUpdateArgs), nil + }, + ) +} + +func (fqu FeeQuoterUpdater[FeeQUpdateArgs]) SequenceDeployOrUpdateFeeQuoter() *cldf_ops.Sequence[FeeQUpdateArgs, sequences.OnChainOutput, cldf_chain.BlockChains] { + return cldf_ops.NewSequence( + "fee-quoter-v1.7.0:update-sequence", + semver.MustParse("1.7.0"), + "Deploys or fetches existing FeeQuoter contract and applies config updates", + func(b cldf_ops.Bundle, chains cldf_chain.BlockChains, input FeeQUpdateArgs) (output sequences.OnChainOutput, err error) { + fqInput, ok := any(input).(sequence1_7.FeeQuoterUpdate) + if !ok { + return sequences.OnChainOutput{}, fmt.Errorf("expected sequence1_7.FeeQuoterUpdate, got %T", input) + } + report, err := cldf_ops.ExecuteSequence(b, sequence1_7.SequenceFeeQuoterUpdate, chains, fqInput) + if err != nil { + return sequences.OnChainOutput{}, err + } + return report.Output, nil + }, + ) +} diff --git a/ccv/chains/evm/deployment/v1_7_0/adapters/init.go b/ccv/chains/evm/deployment/v1_7_0/adapters/init.go new file mode 100644 index 0000000000..25b052303d --- /dev/null +++ b/ccv/chains/evm/deployment/v1_7_0/adapters/init.go @@ -0,0 +1,18 @@ +package adapters + +import ( + "github.com/Masterminds/semver/v3" + chainsel "github.com/smartcontractkit/chain-selectors" + + adapters1_5 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/adapters" + adapters1_6 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/adapters" + "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" +) + +func init() { + fqReg := deploy.GetFQAndRampUpdaterRegistry() + fqReg.RegisterFeeQuoterUpdater(chainsel.FamilyEVM, semver.MustParse("1.7.0"), FeeQuoterUpdater[any]{}) + fqReg.RegisterRampUpdater(chainsel.FamilyEVM, semver.MustParse("1.6.0"), adapters1_6.RampUpdateWithFQ{}) + fqReg.RegisterConfigImporter(chainsel.FamilyEVM, semver.MustParse("1.6.0"), &adapters1_6.ConfigImportAdapter{}) + fqReg.RegisterConfigImporter(chainsel.FamilyEVM, semver.MustParse("1.5.0"), &adapters1_5.ConfigImportAdapter{}) +} diff --git a/ccv/chains/evm/deployment/v1_7_0/operations/fee_quoter/fee_quoter.go b/ccv/chains/evm/deployment/v1_7_0/operations/fee_quoter/fee_quoter.go index 4f01de7bba..78569f6ce1 100644 --- a/ccv/chains/evm/deployment/v1_7_0/operations/fee_quoter/fee_quoter.go +++ b/ccv/chains/evm/deployment/v1_7_0/operations/fee_quoter/fee_quoter.go @@ -9,9 +9,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/gobindings/generated/latest/fee_quoter" + cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract" "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/adapters" - cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment" ) var ContractType cldf_deployment.ContractType = "FeeQuoter" @@ -36,6 +37,13 @@ type ConstructorArgs struct { DestChainConfigArgs []DestChainConfigArgs } +func (a ConstructorArgs) IsEmpty() bool { + return (a.StaticConfig == StaticConfig{}) && + len(a.PriceUpdaters) == 0 && + len(a.TokenTransferFeeConfigArgs) == 0 && + len(a.DestChainConfigArgs) == 0 +} + type ApplyFeeTokensUpdatesArgs struct { FeeTokensToAdd []common.Address FeeTokensToRemove []common.Address diff --git a/ccv/chains/evm/deployment/v1_7_0/sequences/SequenceFeeQuoterInputCreation_Mapping.md b/ccv/chains/evm/deployment/v1_7_0/sequences/SequenceFeeQuoterInputCreation_Mapping.md new file mode 100644 index 0000000000..004d34b2a8 --- /dev/null +++ b/ccv/chains/evm/deployment/v1_7_0/sequences/SequenceFeeQuoterInputCreation_Mapping.md @@ -0,0 +1,55 @@ +# SequenceFeeQuoterInputCreation Mapping Document + +## From v1.6.0 (`CreateFeeQuoterUpdateInputFromV160`) + +**Source Contract**: FeeQuoter v1.6.3 +**Target Contract**: FeeQuoter v1.7.0 + +### Field Mapping +| Target Field (FeeQuoter v1.7.0) | Source Field (FeeQuoter v1.6.3) | Notes | +|----------------------------------|----------------------------------|-------| +| `ConstructorArgs.StaticConfig.LinkToken` | `StaticCfg.LinkToken` | Direct copy | +| `ConstructorArgs.StaticConfig.MaxFeeJuelsPerMsg` | `StaticCfg.MaxFeeJuelsPerMsg` | Direct copy | +| `ConstructorArgs.PriceUpdaters` or `AuthorizedCallerUpdates.AddedCallers` | `PriceUpdaters` | Depends on new deployment vs update | +| `DestChainConfig.IsEnabled` | `RemoteChainCfgs[chain].DestChainCfg.IsEnabled` | Direct copy | +| `DestChainConfig.MaxDataBytes` | `RemoteChainCfgs[chain].DestChainCfg.MaxDataBytes` | Direct copy | +| `DestChainConfig.MaxPerMsgGasLimit` | `RemoteChainCfgs[chain].DestChainCfg.MaxPerMsgGasLimit` | Direct copy | +| `DestChainConfig.DestGasOverhead` | `RemoteChainCfgs[chain].DestChainCfg.DestGasOverhead` | Direct copy | +| `DestChainConfig.DestGasPerPayloadByteBase` | `RemoteChainCfgs[chain].DestChainCfg.DestGasPerPayloadByteBase` | Direct copy | +| `DestChainConfig.ChainFamilySelector` | `RemoteChainCfgs[chain].DestChainCfg.ChainFamilySelector` | Direct copy | +| `DestChainConfig.DefaultTokenFeeUSDCents` | `RemoteChainCfgs[chain].DestChainCfg.DefaultTokenFeeUSDCents` | Direct copy | +| `DestChainConfig.DefaultTokenDestGasOverhead` | `RemoteChainCfgs[chain].DestChainCfg.DefaultTokenDestGasOverhead` | Direct copy | +| `DestChainConfig.DefaultTxGasLimit` | `RemoteChainCfgs[chain].DestChainCfg.DefaultTxGasLimit` | Direct copy | +| `DestChainConfig.NetworkFeeUSDCents` | `RemoteChainCfgs[chain].DestChainCfg.NetworkFeeUSDCents` | Cast from `uint8` to `uint16` | +| `DestChainConfig.LinkFeeMultiplierPercent` | N/A | Hardcoded to `90` | +| `TokenTransferFeeConfig.FeeUSDCents` | `RemoteChainCfgs[chain].TokenTransferFeeCfgs[token].MinFeeUSDCents` | Direct copy | +| `TokenTransferFeeConfig.DestGasOverhead` | `RemoteChainCfgs[chain].TokenTransferFeeCfgs[token].DestGasOverhead` | Direct copy | +| `TokenTransferFeeConfig.DestBytesOverhead` | `RemoteChainCfgs[chain].TokenTransferFeeCfgs[token].DestBytesOverhead` | Direct copy | +| `TokenTransferFeeConfig.IsEnabled` | `RemoteChainCfgs[chain].TokenTransferFeeCfgs[token].IsEnabled` | Direct copy | + +## From v1.5.0 (`CreateFeeQuoterUpdateInputFromV150`) + +**Source Contract**: EVM2EVMOnRamp v1.5.0 +**Target Contract**: FeeQuoter v1.7.0 + +### Field Mapping +| Target Field (FeeQuoter v1.7.0) | Source Field (EVM2EVMOnRamp v1.5.0) | Notes | +|----------------------------------|--------------------------------------|-------| +| `ConstructorArgs.StaticConfig.LinkToken` | `OnRampCfg.StaticConfig.LinkToken` | From first OnRamp (if empty) | +| `ConstructorArgs.StaticConfig.MaxFeeJuelsPerMsg` | `OnRampCfg.StaticConfig.MaxNopFeesJuels` | From first OnRamp (if empty) | +| `ConstructorArgs.PriceUpdaters` | N/A | Empty array `[]` (TODO: what to do with price updaters for 1.5 if there is no 1.6 lanes here) | +| `DestChainConfig.IsEnabled` | N/A | Hardcoded to `true` (if chain is supported on OnRamp, enable it on FeeQuoter) | +| `DestChainConfig.MaxDataBytes` | `OnRampCfg.DynamicConfig.MaxDataBytes` | Direct copy | +| `DestChainConfig.MaxPerMsgGasLimit` | `OnRampCfg.DynamicConfig.MaxPerMsgGasLimit` | Direct copy | +| `DestChainConfig.DestGasOverhead` | `OnRampCfg.DynamicConfig.DestGasOverhead` | Direct copy | +| `DestChainConfig.DestGasPerPayloadByteBase` | `OnRampCfg.DynamicConfig.DestGasPerPayloadByte` | Cast from `uint8` | +| `DestChainConfig.ChainFamilySelector` | N/A | Hardcoded to EVM family selector `0x2812d52c` | +| `DestChainConfig.DefaultTokenFeeUSDCents` | `OnRampCfg.DynamicConfig.DefaultTokenFeeUSDCents` | Direct copy | +| `DestChainConfig.DefaultTokenDestGasOverhead` | `OnRampCfg.DynamicConfig.DefaultTokenDestGasOverhead` | Direct copy | +| `DestChainConfig.DefaultTxGasLimit` | `OnRampCfg.StaticConfig.DefaultTxGasLimit` | Cast to `uint32` | +| `DestChainConfig.NetworkFeeUSDCents` | `OnRampCfg.FeeTokenConfig[].NetworkFeeUSDCents` | From first non-zero value (same across all fee tokens) | +| `DestChainConfig.LinkFeeMultiplierPercent` | N/A | Hardcoded to `90` | +| `TokenTransferFeeConfig.FeeUSDCents` | `OnRampCfg.TokenTransferFeeConfig[token].MinFeeUSDCents` | Direct copy | +| `TokenTransferFeeConfig.DestGasOverhead` | `OnRampCfg.TokenTransferFeeConfig[token].DestGasOverhead` | Direct copy | +| `TokenTransferFeeConfig.DestBytesOverhead` | `OnRampCfg.TokenTransferFeeConfig[token].DestBytesOverhead` | Direct copy | +| `TokenTransferFeeConfig.IsEnabled` | `OnRampCfg.TokenTransferFeeConfig[token].IsEnabled` | Direct copy | diff --git a/ccv/chains/evm/deployment/v1_7_0/sequences/fee_quoter.go b/ccv/chains/evm/deployment/v1_7_0/sequences/fee_quoter.go new file mode 100644 index 0000000000..18ee134254 --- /dev/null +++ b/ccv/chains/evm/deployment/v1_7_0/sequences/fee_quoter.go @@ -0,0 +1,544 @@ +package sequences + +import ( + "encoding/json" + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/gobindings/generated/latest/fee_quoter" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract" + onrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/onramp" + seq1_5 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/sequences" + "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + + datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/adapters" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + mcms_types "github.com/smartcontractkit/mcms/types" + + fq1_6 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/fee_quoter" + seq1_6 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/sequences" + + fqops "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/fee_quoter" +) + +const ( + LinkFeeMultiplierPercent uint8 = 90 + NetworkFeeUSDCents = 10 +) + +type FeeQuoterUpdate struct { + ChainSelector uint64 + ExistingAddresses []datastore.AddressRef + ConstructorArgs fqops.ConstructorArgs + PriceUpdates fqops.PriceUpdates + DestChainConfigs []fqops.DestChainConfigArgs + TokenTransferFeeConfigUpdates fqops.ApplyTokenTransferFeeConfigUpdatesArgs + AuthorizedCallerUpdates fqops.AuthorizedCallerArgs +} + +func (fqu FeeQuoterUpdate) IsEmpty() (bool, error) { + empty := FeeQuoterUpdate{} + // marshal into json + emptyBytes, err := json.Marshal(empty) + if err != nil { + return false, fmt.Errorf("failed to marshal empty FeeQuoterUpdate: %w", err) + } + inputBytes, err := json.Marshal(fqu) + if err != nil { + return false, fmt.Errorf("failed to marshal FeeQuoterUpdate: %w", err) + } + return string(emptyBytes) == string(inputBytes), nil +} + +var ( + // SequenceFeeQuoterUpdate is a sequence that deploys or fetches existing FeeQuoter contract + // and does the following if the corresponding input is provided - + // 1. applies destination chain config updates + // 2. price updates + // 3. token transfer fee config updates + // 4. authorized caller updates + SequenceFeeQuoterUpdate = cldf_ops.NewSequence( + "fee-quoter-v1.7.0:update-sequence", + semver.MustParse("1.7.0"), + "Deploys or fetches existing FeeQuoter contract and applies destination chain config updates and price updates", + func(b cldf_ops.Bundle, chains cldf_chain.BlockChains, input FeeQuoterUpdate) (output sequences.OnChainOutput, err error) { + chain, ok := chains.EVMChains()[input.ChainSelector] + if !ok { + return sequences.OnChainOutput{}, fmt.Errorf("chain with selector %d not found in environment", input.ChainSelector) + } + + // deploy fee quoter or fetch existing fee quoter address + feeQuoterRef, err := contract.MaybeDeployContract( + b, fqops.Deploy, chain, contract.DeployInput[fqops.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(fqops.ContractType, *fqops.Version), + ChainSelector: chain.Selector, + Args: input.ConstructorArgs, + }, input.ExistingAddresses) + if err != nil { + return sequences.OnChainOutput{}, err + } + if feeQuoterRef.Address == "" { + return sequences.OnChainOutput{}, fmt.Errorf("failed to deploy or "+ + "fetch FeeQuoter on chain %s", chain.String()) + } + writes := make([]contract.WriteOutput, 0) + output.Addresses = append(output.Addresses, feeQuoterRef) + fqAddr := common.HexToAddress(feeQuoterRef.Address) + // ApplyDestChainConfigUpdates on FeeQuoter + if len(input.DestChainConfigs) > 0 { + feeQuoterReport, err := cldf_ops.ExecuteOperation( + b, fqops.ApplyDestChainConfigUpdates, chain, + contract.FunctionInput[[]fqops.DestChainConfigArgs]{ + ChainSelector: chain.Selector, + Address: fqAddr, + Args: input.DestChainConfigs, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to apply dest chain "+ + "config updates to FeeQuoter(%s) on chain %s: %w", fqAddr.Hex(), chain, err) + } + writes = append(writes, feeQuoterReport.Output) + } + // update price + if len(input.PriceUpdates.GasPriceUpdates) > 0 || len(input.PriceUpdates.TokenPriceUpdates) > 0 { + feeQuoterUpdatePricesReport, err := cldf_ops.ExecuteOperation( + b, fqops.UpdatePrices, chain, contract.FunctionInput[fqops.PriceUpdates]{ + ChainSelector: chain.Selector, + Address: fqAddr, + Args: fqops.PriceUpdates{ + GasPriceUpdates: input.PriceUpdates.GasPriceUpdates, + TokenPriceUpdates: input.PriceUpdates.TokenPriceUpdates, + }, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to update gas prices on "+ + "FeeQuoter(%s) on chain %s: %w", fqAddr.Hex(), chain, err) + } + writes = append(writes, feeQuoterUpdatePricesReport.Output) + } + // TokenTransferFeeConfigUpdates on FeeQuoter + if len(input.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs) > 0 || + len(input.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs) > 0 { + feeQuoterTokenTransferFeeConfigReport, err := cldf_ops.ExecuteOperation( + b, fqops.ApplyTokenTransferFeeConfigUpdates, chain, + contract.FunctionInput[fqops.ApplyTokenTransferFeeConfigUpdatesArgs]{ + ChainSelector: chain.Selector, + Address: fqAddr, + Args: fqops.ApplyTokenTransferFeeConfigUpdatesArgs{ + TokenTransferFeeConfigArgs: input.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs, + TokensToUseDefaultFeeConfigs: input.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs, + }, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to apply token transfer fee "+ + "config updates to FeeQuoter(%s) on chain %s: %w", fqAddr.Hex(), chain, err) + } + writes = append(writes, feeQuoterTokenTransferFeeConfigReport.Output) + } + // ApplyAuthorizedCallerUpdates on FeeQuoter + if len(input.AuthorizedCallerUpdates.AddedCallers) > 0 || + len(input.AuthorizedCallerUpdates.RemovedCallers) > 0 { + feeQuoterAuthorizedCallerReport, err := cldf_ops.ExecuteOperation( + b, fqops.ApplyAuthorizedCallerUpdates, chain, + contract.FunctionInput[fqops.AuthorizedCallerArgs]{ + ChainSelector: chain.Selector, + Address: fqAddr, + Args: input.AuthorizedCallerUpdates, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to apply authorized caller "+ + "updates to FeeQuoter(%s) on chain %s: %w", fqAddr.Hex(), chain, err) + } + writes = append(writes, feeQuoterAuthorizedCallerReport.Output) + } + batch, err := contract.NewBatchOperationFromWrites(writes) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to create batch operation from writes: %w", err) + } + output.BatchOps = []mcms_types.BatchOperation{batch} + return output, nil + }, + ) + + // CreateFeeQuoterUpdateInputFromV163 creates FeeQuoterUpdate input by importing configuration from FeeQuoter v1.6.0 + CreateFeeQuoterUpdateInputFromV163 = cldf_ops.NewSequence( + "fetches-feequoter-config-values-from-v1.6.3", + semver.MustParse("1.7.0"), + "Creates FeeQuoterUpdate input by importing configuration from FeeQuoter v1.6.3", + func(b cldf_ops.Bundle, chain evm.Chain, input deploy.FeeQuoterUpdateInput) (output FeeQuoterUpdate, err error) { + // check if FeeQuoter v1.6.3 is present in existing addresses, if not, we return empty output + // it means there is no existing fee quoter deployed from v1.6.3 deployment, and we can skip the config import from v1.6.3 + fq16Ref := datastore_utils.GetAddressRef( + input.ExistingAddresses, + input.ChainSelector, + fq1_6.ContractType, + fq1_6.Version, + "", + ) + if datastore_utils.IsAddressRefEmpty(fq16Ref) { + return FeeQuoterUpdate{}, nil + } + output.ChainSelector = input.ChainSelector + output.ExistingAddresses = input.ExistingAddresses + // get feeQuoter 1.6 address meta + metadataForFq16, err := datastore_utils.FilterContractMetaByContractTypeAndVersion( + input.ExistingAddresses, + input.ContractMeta, + fq1_6.ContractType, + fq1_6.Version, + "", + input.ChainSelector, + ) + if err != nil { + return FeeQuoterUpdate{}, fmt.Errorf("failed to get FeeQuoter 1.6.3 address: %w", err) + } + if len(metadataForFq16) == 0 { + return FeeQuoterUpdate{}, fmt.Errorf("no metadata found for FeeQuoter v1.6.3 on chain selector %d", input.ChainSelector) + } + if len(metadataForFq16) > 1 { + return FeeQuoterUpdate{}, fmt.Errorf("multiple metadata entries found for FeeQuoter v1.6.3 on chain selector %d", input.ChainSelector) + } + // Convert metadata to typed struct if needed + fqOutput, err := datastore_utils.ConvertMetadataToType[seq1_6.FeeQuoterImportConfigSequenceOutput](metadataForFq16[0].Metadata) + if err != nil { + return FeeQuoterUpdate{}, fmt.Errorf("failed to convert metadata to "+ + "FeeQuoterImportConfigSequenceOutput for chain selector %d: %w", input.ChainSelector, err) + } + // is feeQuoter going to be deployed or fetched from existing addresses? + feeQuoterRef := datastore_utils.GetAddressRef( + input.ExistingAddresses, + input.ChainSelector, + fqops.ContractType, + fqops.Version, + "", + ) + isNewFQ17Deployment := datastore_utils.IsAddressRefEmpty(feeQuoterRef) + tokenTransferFeeConfigArgs := make([]fee_quoter.FeeQuoterTokenTransferFeeConfigArgs, 0) + allDestChainConfigs := make([]fqops.DestChainConfigArgs, 0) + for remoteChain, cfg := range fqOutput.RemoteChainCfgs { + if !cfg.DestChainCfg.IsEnabled { + continue + } + destChainConfig := cfg.DestChainCfg + outDestchainCfg := fqops.DestChainConfigArgs{ + DestChainSelector: remoteChain, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: destChainConfig.IsEnabled, + MaxDataBytes: destChainConfig.MaxDataBytes, + MaxPerMsgGasLimit: destChainConfig.MaxPerMsgGasLimit, + DestGasOverhead: destChainConfig.DestGasOverhead, + DestGasPerPayloadByteBase: destChainConfig.DestGasPerPayloadByteBase, + ChainFamilySelector: destChainConfig.ChainFamilySelector, + DefaultTokenFeeUSDCents: destChainConfig.DefaultTokenFeeUSDCents, + DefaultTokenDestGasOverhead: destChainConfig.DefaultTokenDestGasOverhead, + DefaultTxGasLimit: destChainConfig.DefaultTxGasLimit, + NetworkFeeUSDCents: uint16(destChainConfig.NetworkFeeUSDCents), + LinkFeeMultiplierPercent: LinkFeeMultiplierPercent, + }, + } + tokenTransferFeeCfgs := make([]fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs, 0) + for token, transferCfg := range cfg.TokenTransferFeeCfgs { + if !transferCfg.IsEnabled { + continue + } + tokenTransferFeeCfgs = append(tokenTransferFeeCfgs, fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + Token: token, + TokenTransferFeeConfig: fee_quoter.FeeQuoterTokenTransferFeeConfig{ + FeeUSDCents: transferCfg.MinFeeUSDCents, + DestGasOverhead: transferCfg.DestGasOverhead, + DestBytesOverhead: transferCfg.DestBytesOverhead, + IsEnabled: transferCfg.IsEnabled, + }, + }) + } + tokenTransferFeeConfigArgs = append(tokenTransferFeeConfigArgs, fee_quoter.FeeQuoterTokenTransferFeeConfigArgs{ + DestChainSelector: remoteChain, + TokenTransferFeeConfigs: tokenTransferFeeCfgs, + }) + allDestChainConfigs = append(allDestChainConfigs, outDestchainCfg) + } + if isNewFQ17Deployment { + output.ConstructorArgs = fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: fqOutput.StaticCfg.LinkToken, + MaxFeeJuelsPerMsg: fqOutput.StaticCfg.MaxFeeJuelsPerMsg, + }, + PriceUpdaters: fqOutput.PriceUpdaters, + TokenTransferFeeConfigArgs: tokenTransferFeeConfigArgs, + DestChainConfigArgs: allDestChainConfigs, + } + } else { + output.AuthorizedCallerUpdates = fqops.AuthorizedCallerArgs{ + AddedCallers: fqOutput.PriceUpdaters, + } + output.TokenTransferFeeConfigUpdates = fqops.ApplyTokenTransferFeeConfigUpdatesArgs{ + TokenTransferFeeConfigArgs: tokenTransferFeeConfigArgs, + } + output.DestChainConfigs = allDestChainConfigs + } + return output, nil + }) + + // CreateFeeQuoterUpdateInputFromV150 creates FeeQuoterUpdate input by importing configuration from PriceRegistry v1.5.0 and EVM2EVMOnRamp v1.5.0 + CreateFeeQuoterUpdateInputFromV150 = cldf_ops.NewSequence( + "fetches-feequoter-config-values-from-v1.5.0", + semver.MustParse("1.7.0"), + "Creates FeeQuoterUpdate input by importing configuration from PriceRegistry v1.5.0 and EVM2EVMOnRamp v1.5.0", + func(b cldf_ops.Bundle, chain evm.Chain, input deploy.FeeQuoterUpdateInput) (output FeeQuoterUpdate, err error) { + // get addressref for onramp 1.5.0 + onRampRef := datastore_utils.GetAddressRef( + input.ExistingAddresses, + input.ChainSelector, + onrampops.ContractType, + onrampops.Version, + "", + ) + // if there is no address ref for onRamp 1.5.0, it means onRamp 1.5.0 is not deployed and we can skip the config import from onRamp 1.5.0 + if datastore_utils.IsAddressRefEmpty(onRampRef) { + return FeeQuoterUpdate{}, nil + } + // get address meta for onRamp 1.5.0 to read the config values from onRamp 1.5.0 + onRampMetadata, err := datastore_utils.FilterContractMetaByContractTypeAndVersion( + input.ExistingAddresses, + input.ContractMeta, + onrampops.ContractType, + onrampops.Version, + "", + input.ChainSelector, + ) + if err != nil { + return FeeQuoterUpdate{}, fmt.Errorf("failed to get EVM2EVMOnRamp v1.5.0 address: %w", err) + } + if len(onRampMetadata) == 0 { + return FeeQuoterUpdate{}, fmt.Errorf("no metadata found for EVM2EVMOnRamp v1.5.0 on chain selector %d", input.ChainSelector) + } + output.ChainSelector = input.ChainSelector + output.ExistingAddresses = input.ExistingAddresses + // is feeQuoter going to be deployed or fetched from existing addresses? + feeQuoter17Ref := datastore_utils.GetAddressRef( + input.ExistingAddresses, + input.ChainSelector, + fqops.ContractType, + fqops.Version, + "", + ) + isNewFQ17Deployment := datastore_utils.IsAddressRefEmpty(feeQuoter17Ref) + var staticCfg fqops.StaticConfig + var destChainCfgs []fqops.DestChainConfigArgs + var tokenTransferFeeConfigArgs []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs + var tokenTransferFeeConfigArgsForAll []fqops.TokenTransferFeeConfigArgs + for _, meta := range onRampMetadata { + // Convert metadata to typed struct if needed + onRampCfg, err := datastore_utils.ConvertMetadataToType[seq1_5.OnRampImportConfigSequenceOutput](meta.Metadata) + if err != nil { + return FeeQuoterUpdate{}, fmt.Errorf("failed to convert metadata to "+ + "OnRampImportConfigSequenceOutput for chain selector %d: %w", input.ChainSelector, err) + } + if staticCfg.LinkToken == (common.Address{}) { + staticCfg = fqops.StaticConfig{ + LinkToken: onRampCfg.StaticConfig.LinkToken, + MaxFeeJuelsPerMsg: onRampCfg.StaticConfig.MaxNopFeesJuels, + } + } + + chainFamilySelectorBytes := utils.GetSelectorHex(onRampCfg.RemoteChainSelector) + // Safely convert ChainFamilySelector from []byte to [4]byte + var chainFamilySelector [4]byte + if len(chainFamilySelectorBytes) < 4 { + return FeeQuoterUpdate{}, fmt.Errorf("ChainFamilySelector has invalid length %d (expected 4) for remote chain selector %d", len(chainFamilySelectorBytes), onRampCfg.RemoteChainSelector) + } + copy(chainFamilySelector[:], chainFamilySelectorBytes[:4]) + destChainCfgs = append(destChainCfgs, fqops.DestChainConfigArgs{ + DestChainSelector: onRampCfg.RemoteChainSelector, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, // if the chain is supported on OnRamp, we should enable it on FeeQuoter + MaxDataBytes: onRampCfg.DynamicConfig.MaxDataBytes, + MaxPerMsgGasLimit: onRampCfg.DynamicConfig.MaxPerMsgGasLimit, + DestGasOverhead: onRampCfg.DynamicConfig.DestGasOverhead, + DestGasPerPayloadByteBase: uint8(onRampCfg.DynamicConfig.DestGasPerPayloadByte), + ChainFamilySelector: chainFamilySelector, + DefaultTokenFeeUSDCents: onRampCfg.DynamicConfig.DefaultTokenFeeUSDCents, + DefaultTokenDestGasOverhead: onRampCfg.DynamicConfig.DefaultTokenDestGasOverhead, + DefaultTxGasLimit: uint32(onRampCfg.StaticConfig.DefaultTxGasLimit), + NetworkFeeUSDCents: NetworkFeeUSDCents, + LinkFeeMultiplierPercent: LinkFeeMultiplierPercent, + }, + }) + for token, tokenCfg := range onRampCfg.TokenTransferFeeConfig { + tokenTransferFeeConfigArgs = append(tokenTransferFeeConfigArgs, fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + Token: token, + TokenTransferFeeConfig: fee_quoter.FeeQuoterTokenTransferFeeConfig{ + FeeUSDCents: tokenCfg.MinFeeUSDCents, + DestGasOverhead: tokenCfg.DestGasOverhead, + DestBytesOverhead: tokenCfg.DestBytesOverhead, + IsEnabled: tokenCfg.IsEnabled, + }, + }) + } + tokenTransferFeeConfigArgsForAll = append(tokenTransferFeeConfigArgsForAll, fqops.TokenTransferFeeConfigArgs{ + DestChainSelector: onRampCfg.RemoteChainSelector, + TokenTransferFeeConfigs: tokenTransferFeeConfigArgs, + }) + } + if isNewFQ17Deployment { + output.ConstructorArgs = fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: staticCfg.LinkToken, + MaxFeeJuelsPerMsg: staticCfg.MaxFeeJuelsPerMsg, + }, + DestChainConfigArgs: destChainCfgs, + TokenTransferFeeConfigArgs: tokenTransferFeeConfigArgsForAll, + // TODO: what to do with price updaters for 1.5 if there is no 1.6 lanes here + // PriceUpdaters: []common.Address{}, + } + } else { + output.DestChainConfigs = destChainCfgs + output.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs = tokenTransferFeeConfigArgsForAll + } + return output, nil + }) +) + +// MergeFeeQuoterUpdateOutputs merges FeeQuoterUpdate outputs from the v1.6.3 and v1.5.0 import +// sequences into a single update. output16 is the base; output15 supplements it. Where both +// provide values (e.g. ConstructorArgs, dest chain configs, token transfer fee configs), +// output16 takes precedence and output15 fills in only missing entries. +func MergeFeeQuoterUpdateOutputs(output16, output15 FeeQuoterUpdate) (FeeQuoterUpdate, error) { + result := output16 + + // ConstructorArgs: use output15 if output16 is empty + if result.ConstructorArgs.IsEmpty() { + result.ConstructorArgs = output15.ConstructorArgs + } else { + // merge the dest chainConfig args + result.ConstructorArgs.DestChainConfigArgs = mergeDestChainConfigs( + result.ConstructorArgs.DestChainConfigArgs, + output15.ConstructorArgs.DestChainConfigArgs) + resultPriceUpdatersMap := make(map[common.Address]bool) + for _, updater := range result.ConstructorArgs.PriceUpdaters { + resultPriceUpdatersMap[updater] = true + } + for _, updater := range output15.ConstructorArgs.PriceUpdaters { + if !resultPriceUpdatersMap[updater] { + result.ConstructorArgs.PriceUpdaters = append(result.ConstructorArgs.PriceUpdaters, updater) + resultPriceUpdatersMap[updater] = true + } + } + result.ConstructorArgs.TokenTransferFeeConfigArgs = mergeTokenTransferFeeConfigArgs( + result.ConstructorArgs.TokenTransferFeeConfigArgs, + output15.ConstructorArgs.TokenTransferFeeConfigArgs) + result.ConstructorArgs.PriceUpdaters = maps.Keys(resultPriceUpdatersMap) + } + + result.DestChainConfigs = mergeDestChainConfigs(result.DestChainConfigs, output15.DestChainConfigs) + + // TokenTransferFeeConfigUpdates: merge by DestChainSelector, output16 takes precedence for duplicates + result.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs = mergeTokenTransferFeeConfigArgs( + result.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs, + output15.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs) + + // TokensToUseDefaultFeeConfigs: merge by DestChainSelector and Token + if len(result.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs) == 0 { + result.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs = output15.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs + } else { + // Create a map of (DestChainSelector, Token) pairs from output16 + tokenRemoveMap := make(map[string]bool) + for _, cfg := range result.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs { + key := fmt.Sprintf("%d:%s", cfg.DestChainSelector, cfg.Token.Hex()) + tokenRemoveMap[key] = true + } + // Add configs from output15 that don't exist in output16 + for _, cfg := range output15.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs { + key := fmt.Sprintf("%d:%s", cfg.DestChainSelector, cfg.Token.Hex()) + if !tokenRemoveMap[key] { + result.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs = append(result.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs, cfg) + } + // If it exists in both, output16's value is already used (takes precedence) + } + } + + // AuthorizedCallerUpdates: merge unique entries from both outputs + result.AuthorizedCallerUpdates = mergePriceUpdaters(result.AuthorizedCallerUpdates, output15.AuthorizedCallerUpdates) + + return result, nil +} + +func mergeTokenTransferFeeConfigArgs(args1, args2 []fee_quoter.FeeQuoterTokenTransferFeeConfigArgs) []fee_quoter.FeeQuoterTokenTransferFeeConfigArgs { + result := args1 + // TokenTransferFeeConfigArgs: merge by DestChainSelector + if len(result) == 0 { + result = args2 + } else { + // Create a map of dest chain selectors from output16 + tokenConfigMap := make(map[uint64]int) + for i, cfg := range result { + tokenConfigMap[cfg.DestChainSelector] = i + } + // Add configs from output15 that don't exist in output16 + for _, cfg := range args2 { + if _, exists := tokenConfigMap[cfg.DestChainSelector]; !exists && len(cfg.TokenTransferFeeConfigs) > 0 { + result = append(result, cfg) + } + // If it exists in both, output16's value is already used (takes precedence) + } + } + return result +} + +func mergePriceUpdaters(updaters1, updaters2 fqops.AuthorizedCallerArgs) fqops.AuthorizedCallerArgs { + result := updaters1 + // AddedCallers: merge unique addresses from both outputs + addedCallersMap := make(map[common.Address]bool) + for _, addr := range result.AddedCallers { + addedCallersMap[addr] = true + } + for _, addr := range updaters2.AddedCallers { + if !addedCallersMap[addr] { + result.AddedCallers = append(result.AddedCallers, addr) + addedCallersMap[addr] = true + } + } + // RemovedCallers: merge unique addresses from both outputs + removedCallersMap := make(map[common.Address]bool) + for _, addr := range result.RemovedCallers { + removedCallersMap[addr] = true + } + for _, addr := range updaters2.RemovedCallers { + if !removedCallersMap[addr] { + result.RemovedCallers = append(result.RemovedCallers, addr) + removedCallersMap[addr] = true + } + } + return result +} + +func mergeDestChainConfigs(cfgs1, cfgs2 []fqops.DestChainConfigArgs) []fqops.DestChainConfigArgs { + // Create a map of dest chain selectors from cfgs1 + destChainMap := make(map[uint64]fqops.DestChainConfigArgs) + for _, cfg := range cfgs1 { + destChainMap[cfg.DestChainSelector] = cfg + } + result := cfgs1 + // Add configs from cfgs2 that don't exist in cfgs1 + for _, cfg := range cfgs2 { + if _, exists := destChainMap[cfg.DestChainSelector]; !exists { + result = append(result, cfg) + } + // If it exists in both, cfgs1's value is already used (takes precedence) + } + if len(destChainMap) == 0 { + return nil + } + return result +} diff --git a/ccv/chains/evm/deployment/v1_7_0/sequences/fee_quoter_test.go b/ccv/chains/evm/deployment/v1_7_0/sequences/fee_quoter_test.go new file mode 100644 index 0000000000..c80c177ebb --- /dev/null +++ b/ccv/chains/evm/deployment/v1_7_0/sequences/fee_quoter_test.go @@ -0,0 +1,403 @@ +package sequences_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/gobindings/generated/latest/fee_quoter" + "github.com/stretchr/testify/require" + + fqops "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/fee_quoter" + sequence1_7 "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/sequences" + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/adapters" +) + +func TestFeeQuoterUpdate_IsEmpty(t *testing.T) { + empty := sequence1_7.FeeQuoterUpdate{} + isEmpty, err := empty.IsEmpty() + require.NoError(t, err) + require.True(t, isEmpty, "Empty FeeQuoterUpdate should return true") + + nonEmpty := sequence1_7.FeeQuoterUpdate{ + ChainSelector: 1, + } + isEmpty, err = nonEmpty.IsEmpty() + require.NoError(t, err) + require.False(t, isEmpty, "Non-empty FeeQuoterUpdate should return false") +} + +func TestMergeFeeQuoterUpdateOutputs(t *testing.T) { + addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111") + addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222") + addr3 := common.HexToAddress("0x3333333333333333333333333333333333333333") + addr4 := common.HexToAddress("0x4444444444444444444444444444444444444444") + addr5 := common.HexToAddress("0x5555555555555555555555555555555555555555") + + t.Run("empty outputs", func(t *testing.T) { + output16 := sequence1_7.FeeQuoterUpdate{} + output15 := sequence1_7.FeeQuoterUpdate{} + result, err := sequence1_7.MergeFeeQuoterUpdateOutputs(output16, output15) + require.NoError(t, err) + require.Equal(t, sequence1_7.FeeQuoterUpdate{}, result) + }) + + t.Run("ConstructorArgs - output15 used when output16 is empty", func(t *testing.T) { + linkToken := common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA") + maxFeeJuelsPerMsg := big.NewInt(1000000000000000000) // 1 LINK + output16 := sequence1_7.FeeQuoterUpdate{ + ConstructorArgs: fqops.ConstructorArgs{}, // empty + } + output15 := sequence1_7.FeeQuoterUpdate{ + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: linkToken, + MaxFeeJuelsPerMsg: maxFeeJuelsPerMsg, + }, + }, + } + result, err := sequence1_7.MergeFeeQuoterUpdateOutputs(output16, output15) + require.NoError(t, err) + require.Equal(t, linkToken, result.ConstructorArgs.StaticConfig.LinkToken) + require.Equal(t, maxFeeJuelsPerMsg, result.ConstructorArgs.StaticConfig.MaxFeeJuelsPerMsg) + }) + + t.Run("ConstructorArgs - output16 takes precedence when not empty", func(t *testing.T) { + linkToken16 := common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA") + linkToken15 := common.HexToAddress("0x326C977E6efc84E512bB9C30f76E30c160eD06FB") + maxFeeJuelsPerMsg16 := big.NewInt(2000000000000000000) // 2 LINK + maxFeeJuelsPerMsg15 := big.NewInt(1000000000000000000) // 1 LINK + output16 := sequence1_7.FeeQuoterUpdate{ + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: linkToken16, + MaxFeeJuelsPerMsg: maxFeeJuelsPerMsg16, + }, + }, + } + output15 := sequence1_7.FeeQuoterUpdate{ + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: linkToken15, + MaxFeeJuelsPerMsg: maxFeeJuelsPerMsg15, + }, + }, + } + result, err := sequence1_7.MergeFeeQuoterUpdateOutputs(output16, output15) + require.NoError(t, err) + // output16's StaticConfig should be used (takes precedence) + require.Equal(t, linkToken16, result.ConstructorArgs.StaticConfig.LinkToken) + require.Equal(t, maxFeeJuelsPerMsg16, result.ConstructorArgs.StaticConfig.MaxFeeJuelsPerMsg) + }) + + t.Run("ConstructorArgs - merge DestChainConfig,PriceUpdaters TokenTransferFeeConfigArgs", func(t *testing.T) { + output16 := sequence1_7.FeeQuoterUpdate{ + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA"), + MaxFeeJuelsPerMsg: big.NewInt(2000000000000000000), // 2 LINK + }, + PriceUpdaters: []common.Address{addr1, addr2}, + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + { + DestChainSelector: 100, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + {Token: addr1}, + }, + }, + }, + DestChainConfigArgs: []fqops.DestChainConfigArgs{ + { + DestChainSelector: 100, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 1000, + }, + }, + }, + }, + } + output15 := sequence1_7.FeeQuoterUpdate{ + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: common.HexToAddress("0x326C977E6efc84E512bB9C30f76E30c160eD06FB"), + MaxFeeJuelsPerMsg: big.NewInt(1000000000000000000), // 1 LINK + }, + PriceUpdaters: []common.Address{addr2, addr3}, // addr2 is duplicate + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + { + DestChainSelector: 100, // duplicate selector + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + {Token: addr2}, + }, + }, + { + DestChainSelector: 200, // unique selector + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + {Token: addr3}, + }, + }, + }, + DestChainConfigArgs: []fqops.DestChainConfigArgs{ + { + DestChainSelector: 100, // duplicate selector + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: false, + MaxDataBytes: 2000, + }, + }, + { + DestChainSelector: 200, // unique selector + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 3000, + }, + }, + }, + }, + } + expected := sequence1_7.FeeQuoterUpdate{ + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA"), + MaxFeeJuelsPerMsg: big.NewInt(2000000000000000000), // from output16 + }, + PriceUpdaters: []common.Address{addr1, addr2, addr3}, // merged with duplicates removed + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + { + DestChainSelector: 100, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + {Token: addr1}, // from output16 (takes precedence) + }, + }, + { + DestChainSelector: 200, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + {Token: addr3}, // from output15 + }, + }, + }, + DestChainConfigArgs: []fqops.DestChainConfigArgs{ + { + DestChainSelector: 100, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 1000, + }, + }, + { + DestChainSelector: 200, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 3000, + }, + }, + }, + }, + } + result, err := sequence1_7.MergeFeeQuoterUpdateOutputs(output16, output15) + require.NoError(t, err) + require.Equal(t, expected.ConstructorArgs, result.ConstructorArgs) + }) + + t.Run("DestChainConfigs - output16 takes precedence for duplicates", func(t *testing.T) { + output16 := sequence1_7.FeeQuoterUpdate{ + DestChainConfigs: []fqops.DestChainConfigArgs{ + { + DestChainSelector: 100, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 1000, + }, + }, + }, + } + output15 := sequence1_7.FeeQuoterUpdate{ + DestChainConfigs: []fqops.DestChainConfigArgs{ + { + DestChainSelector: 100, // duplicate selector + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: false, + MaxDataBytes: 2000, + }, + }, + { + DestChainSelector: 200, // unique selector + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 3000, + }, + }, + }, + } + result, err := sequence1_7.MergeFeeQuoterUpdateOutputs(output16, output15) + require.NoError(t, err) + require.Len(t, result.DestChainConfigs, 2) + // output16's config for selector 100 should be used + require.Equal(t, uint64(100), result.DestChainConfigs[0].DestChainSelector) + require.True(t, result.DestChainConfigs[0].DestChainConfig.IsEnabled) + require.Equal(t, uint32(1000), result.DestChainConfigs[0].DestChainConfig.MaxDataBytes) + // output15's config for selector 200 should be added + require.Equal(t, uint64(200), result.DestChainConfigs[1].DestChainSelector) + require.Equal(t, uint32(3000), result.DestChainConfigs[1].DestChainConfig.MaxDataBytes) + }) + + t.Run("TokenTransferFeeConfigArgs - output16 takes precedence for duplicates", func(t *testing.T) { + output16 := sequence1_7.FeeQuoterUpdate{ + TokenTransferFeeConfigUpdates: fqops.ApplyTokenTransferFeeConfigUpdatesArgs{ + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + { + DestChainSelector: 100, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + {Token: addr1}, + }, + }, + }, + }, + } + output15 := sequence1_7.FeeQuoterUpdate{ + TokenTransferFeeConfigUpdates: fqops.ApplyTokenTransferFeeConfigUpdatesArgs{ + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + { + DestChainSelector: 100, // duplicate + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + {Token: addr2}, + }, + }, + { + DestChainSelector: 200, // unique + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + {Token: addr3}, + }, + }, + }, + }, + } + result, err := sequence1_7.MergeFeeQuoterUpdateOutputs(output16, output15) + require.NoError(t, err) + require.Len(t, result.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs, 2) + // output16's config for selector 100 should be used + require.Equal(t, uint64(100), result.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs[0].DestChainSelector) + require.Equal(t, addr1, result.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs[0].TokenTransferFeeConfigs[0].Token) + // output15's config for selector 200 should be added + require.Equal(t, uint64(200), result.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs[1].DestChainSelector) + require.Equal(t, addr3, result.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs[1].TokenTransferFeeConfigs[0].Token) + }) + + t.Run("TokensToUseDefaultFeeConfigs - merge by DestChainSelector and Token", func(t *testing.T) { + output16 := sequence1_7.FeeQuoterUpdate{ + TokenTransferFeeConfigUpdates: fqops.ApplyTokenTransferFeeConfigUpdatesArgs{ + TokensToUseDefaultFeeConfigs: []fqops.TokenTransferFeeConfigRemoveArgs{ + {DestChainSelector: 100, Token: addr1}, + {DestChainSelector: 100, Token: addr2}, + }, + }, + } + output15 := sequence1_7.FeeQuoterUpdate{ + TokenTransferFeeConfigUpdates: fqops.ApplyTokenTransferFeeConfigUpdatesArgs{ + TokensToUseDefaultFeeConfigs: []fqops.TokenTransferFeeConfigRemoveArgs{ + {DestChainSelector: 100, Token: addr2}, // duplicate + {DestChainSelector: 200, Token: addr3}, // unique + }, + }, + } + result, err := sequence1_7.MergeFeeQuoterUpdateOutputs(output16, output15) + require.NoError(t, err) + require.Len(t, result.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs, 3) + // Verify all expected entries are present + require.Contains(t, result.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs, + fqops.TokenTransferFeeConfigRemoveArgs{DestChainSelector: 100, Token: addr1}) + require.Contains(t, result.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs, + fqops.TokenTransferFeeConfigRemoveArgs{DestChainSelector: 100, Token: addr2}) + require.Contains(t, result.TokenTransferFeeConfigUpdates.TokensToUseDefaultFeeConfigs, + fqops.TokenTransferFeeConfigRemoveArgs{DestChainSelector: 200, Token: addr3}) + }) + + t.Run("AuthorizedCallerUpdates - merge unique entries", func(t *testing.T) { + output16 := sequence1_7.FeeQuoterUpdate{ + AuthorizedCallerUpdates: fqops.AuthorizedCallerArgs{ + AddedCallers: []common.Address{addr1, addr2}, + RemovedCallers: []common.Address{addr3}, + }, + } + output15 := sequence1_7.FeeQuoterUpdate{ + AuthorizedCallerUpdates: fqops.AuthorizedCallerArgs{ + AddedCallers: []common.Address{addr2, addr4}, // addr2 is duplicate + RemovedCallers: []common.Address{addr3, addr5}, // addr3 is duplicate + }, + } + result, err := sequence1_7.MergeFeeQuoterUpdateOutputs(output16, output15) + require.NoError(t, err) + require.Len(t, result.AuthorizedCallerUpdates.AddedCallers, 3) + require.Contains(t, result.AuthorizedCallerUpdates.AddedCallers, addr1) + require.Contains(t, result.AuthorizedCallerUpdates.AddedCallers, addr2) + require.Contains(t, result.AuthorizedCallerUpdates.AddedCallers, addr4) + require.Len(t, result.AuthorizedCallerUpdates.RemovedCallers, 2) + require.Contains(t, result.AuthorizedCallerUpdates.RemovedCallers, addr3) + require.Contains(t, result.AuthorizedCallerUpdates.RemovedCallers, addr5) + }) + + t.Run("comprehensive merge", func(t *testing.T) { + linkToken16 := common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA") + linkToken15 := common.HexToAddress("0x326C977E6efc84E512bB9C30f76E30c160eD06FB") + maxFeeJuelsPerMsg16 := big.NewInt(2000000000000000000) // 2 LINK + maxFeeJuelsPerMsg15 := big.NewInt(1000000000000000000) // 1 LINK + output16 := sequence1_7.FeeQuoterUpdate{ + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: linkToken16, + MaxFeeJuelsPerMsg: maxFeeJuelsPerMsg16, + }, + }, + DestChainConfigs: []fqops.DestChainConfigArgs{ + {DestChainSelector: 200, DestChainConfig: adapters.FeeQuoterDestChainConfig{IsEnabled: true}}, + }, + TokenTransferFeeConfigUpdates: fqops.ApplyTokenTransferFeeConfigUpdatesArgs{ + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + {DestChainSelector: 400, TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{{Token: addr1}}}, + }, + }, + AuthorizedCallerUpdates: fqops.AuthorizedCallerArgs{ + AddedCallers: []common.Address{addr1}, + }, + } + output15 := sequence1_7.FeeQuoterUpdate{ + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: linkToken15, + MaxFeeJuelsPerMsg: maxFeeJuelsPerMsg15, + }, + }, + DestChainConfigs: []fqops.DestChainConfigArgs{ + {DestChainSelector: 200, DestChainConfig: adapters.FeeQuoterDestChainConfig{IsEnabled: false}}, // duplicate + {DestChainSelector: 300, DestChainConfig: adapters.FeeQuoterDestChainConfig{IsEnabled: true}}, // unique + }, + TokenTransferFeeConfigUpdates: fqops.ApplyTokenTransferFeeConfigUpdatesArgs{ + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + {DestChainSelector: 400, TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{{Token: addr2}}}, // duplicate + {DestChainSelector: 500, TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{{Token: addr3}}}, // unique + }, + }, + AuthorizedCallerUpdates: fqops.AuthorizedCallerArgs{ + AddedCallers: []common.Address{addr2, addr3}, + }, + } + result, err := sequence1_7.MergeFeeQuoterUpdateOutputs(output16, output15) + require.NoError(t, err) + // ConstructorArgs from output16 (not empty) - StaticConfig takes precedence + require.Equal(t, linkToken16, result.ConstructorArgs.StaticConfig.LinkToken) + require.Equal(t, maxFeeJuelsPerMsg16, result.ConstructorArgs.StaticConfig.MaxFeeJuelsPerMsg) + // DestChainConfigs: output16's config for 200, plus output15's config for 300 + require.Len(t, result.DestChainConfigs, 2) + require.True(t, result.DestChainConfigs[0].DestChainConfig.IsEnabled) // from output16 + // TokenTransferFeeConfigArgs: output16's config for 400, plus output15's config for 500 + require.Len(t, result.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs, 2) + require.Equal(t, uint64(400), result.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs[0].DestChainSelector) + require.Equal(t, addr1, result.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs[0].TokenTransferFeeConfigs[0].Token) // from output16 + // AuthorizedCallerUpdates: merged unique entries + require.Len(t, result.AuthorizedCallerUpdates.AddedCallers, 3) + require.Contains(t, result.AuthorizedCallerUpdates.AddedCallers, addr1) + require.Contains(t, result.AuthorizedCallerUpdates.AddedCallers, addr2) + require.Contains(t, result.AuthorizedCallerUpdates.AddedCallers, addr3) + }) +} diff --git a/ccv/chains/evm/deployment/v1_7_0/sequences/sequence_fee_quoter_input_creation_test.go b/ccv/chains/evm/deployment/v1_7_0/sequences/sequence_fee_quoter_input_creation_test.go new file mode 100644 index 0000000000..b43a5ab377 --- /dev/null +++ b/ccv/chains/evm/deployment/v1_7_0/sequences/sequence_fee_quoter_input_creation_test.go @@ -0,0 +1,922 @@ +package sequences_test + +import ( + "math/big" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/stretchr/testify/require" + + evmadapter "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/adapters" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/adapters" + + "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/gobindings/generated/latest/fee_quoter" + + fqops "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/fee_quoter" + sequence1_7 "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/sequences" + "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" + + evm_2_evm_onramp_v1_5_0 "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/evm_2_evm_onramp" + fee_quoter_v1_6_3 "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_6_3/fee_quoter" + + seq1_5 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/sequences" + seq1_6 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/sequences" +) + +// dummyAddressRefs is hardcoded address refs (previously from address_refs.json). +// Chain selectors must match dummyContractMetadata so metadata lookup succeeds. +var dummyAddressRefs = []datastore.AddressRef{ + {Address: "0x1111111111111111111111111111111111111111", ChainSelector: 5009297550715157269, Type: datastore.ContractType("FeeQuoter"), Version: semver.MustParse("1.6.3")}, + {Address: "0x6666666666666666666666666666666666666666", ChainSelector: 5009297550715157269, Type: datastore.ContractType("EVM2EVMOnRamp"), Version: semver.MustParse("1.5.0")}, + {Address: "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", ChainSelector: 4949039107694359620, Type: datastore.ContractType("FeeQuoter"), Version: semver.MustParse("1.6.3")}, + {Address: "0x1010101010101010101010101010101010101010", ChainSelector: 4949039107694359620, Type: datastore.ContractType("EVM2EVMOnRamp"), Version: semver.MustParse("1.5.0")}, + {Address: "0x5050505050505050505050505050505050505050", ChainSelector: 15971525489660198786, Type: datastore.ContractType("EVM2EVMOnRamp"), Version: semver.MustParse("1.5.0")}, + {Address: "0x6060606060606060606060606060606060606060", ChainSelector: 5936861837188149645, Type: datastore.ContractType("FeeQuoter"), Version: semver.MustParse("1.6.3")}, + {Address: "0x7070707070707070707070707070707070707070", ChainSelector: 5936861837188149645, Type: datastore.ContractType("EVM2EVMOnRamp"), Version: semver.MustParse("1.5.0")}, +} + +var dummyContractMetadata = []datastore.ContractMetadata{ + { + Address: "0x1111111111111111111111111111111111111111", + ChainSelector: 5009297550715157269, + Metadata: seq1_6.FeeQuoterImportConfigSequenceOutput{ + RemoteChainCfgs: map[uint64]seq1_6.FeeQuoterImportConfigSequenceOutputPerRemoteChain{ + 15971525489660198786: { + DestChainCfg: fee_quoter_v1_6_3.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxNumberOfTokensPerMsg: 3, + MaxDataBytes: 8000, + MaxPerMsgGasLimit: 4000000, + DestGasOverhead: 80000, + DestGasPerPayloadByteBase: 14, + DestGasPerPayloadByteHigh: 28, + DestGasPerPayloadByteThreshold: 800, + DestDataAvailabilityOverheadGas: 40000, + DestGasPerDataAvailabilityByte: 8, + DestDataAvailabilityMultiplierBps: 900, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(15971525489660198786)), + EnforceOutOfOrder: false, + DefaultTokenFeeUSDCents: 8, + DefaultTokenDestGasOverhead: 40000, + DefaultTxGasLimit: 180000, + GasMultiplierWeiPerEth: 0, + GasPriceStalenessThreshold: 0, + NetworkFeeUSDCents: 4, + }, + TokenTransferFeeCfgs: map[common.Address]fee_quoter_v1_6_3.FeeQuoterTokenTransferFeeConfig{ + common.HexToAddress("0x2222222222222222222222222222222222222222"): { + MinFeeUSDCents: 4, + MaxFeeUSDCents: 40, + DeciBps: 90, + DestGasOverhead: 25000, + DestBytesOverhead: 80, + IsEnabled: true, + }, + }, + }, + }, + StaticCfg: fee_quoter_v1_6_3.FeeQuoterStaticConfig{ + MaxFeeJuelsPerMsg: big.NewInt(1000000000000000000), + LinkToken: common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA"), + TokenPriceStalenessThreshold: 3600, + }, + PriceUpdaters: []common.Address{ + common.HexToAddress("0x4444444444444444444444444444444444444444"), + common.HexToAddress("0x5555555555555555555555555555555555555555"), + }, + }, + }, + { + Address: "0x6666666666666666666666666666666666666666", + ChainSelector: 5009297550715157269, + Metadata: seq1_5.OnRampImportConfigSequenceOutput{ + RemoteChainSelector: 4949039107694359620, + StaticConfig: evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampStaticConfig{ + LinkToken: common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA"), + ChainSelector: 5009297550715157269, + DestChainSelector: 4949039107694359620, + DefaultTxGasLimit: 200000, + MaxNopFeesJuels: big.NewInt(1000000000000000000), + PrevOnRamp: common.HexToAddress("0x0000000000000000000000000000000000000000"), + RmnProxy: common.HexToAddress("0x7777777777777777777777777777777777777777"), + TokenAdminRegistry: common.HexToAddress("0x8888888888888888888888888888888888888888"), + }, + DynamicConfig: evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampDynamicConfig{ + Router: common.HexToAddress("0x9999999999999999999999999999999999999999"), + MaxNumberOfTokensPerMsg: 5, + DestGasOverhead: 100000, + DestGasPerPayloadByte: 16, + DestDataAvailabilityOverheadGas: 50000, + DestGasPerDataAvailabilityByte: 10, + DestDataAvailabilityMultiplierBps: 1000, + PriceRegistry: common.HexToAddress("0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + MaxDataBytes: 10000, + MaxPerMsgGasLimit: 5000000, + DefaultTokenFeeUSDCents: 0, + DefaultTokenDestGasOverhead: 0, + EnforceOutOfOrder: false, + }, + TokenTransferFeeConfig: map[common.Address]evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampTokenTransferFeeConfig{ + common.HexToAddress("0x2222222222222222222222222222222222222222"): { + MinFeeUSDCents: 5, + MaxFeeUSDCents: 50, + DeciBps: 100, + DestGasOverhead: 30000, + DestBytesOverhead: 100, + AggregateRateLimitEnabled: false, + IsEnabled: true, + }, + common.HexToAddress("0x3333333333333333333333333333333333333333"): { + MinFeeUSDCents: 10, + MaxFeeUSDCents: 100, + DeciBps: 200, + DestGasOverhead: 40000, + DestBytesOverhead: 200, + AggregateRateLimitEnabled: false, + IsEnabled: true, + }, + }, + FeeTokenConfig: map[common.Address]evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampFeeTokenConfig{ + common.HexToAddress("0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"): { + NetworkFeeUSDCents: 5, + GasMultiplierWeiPerEth: 1000000000000000000, + PremiumMultiplierWeiPerEth: 1100000000000000000, + Enabled: true, + }, + common.HexToAddress("0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"): { + NetworkFeeUSDCents: 3, + GasMultiplierWeiPerEth: 900000000000000000, + PremiumMultiplierWeiPerEth: 1000000000000000000, + Enabled: true, + }, + }, + }, + }, + { + Address: "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + ChainSelector: 4949039107694359620, + Metadata: seq1_6.FeeQuoterImportConfigSequenceOutput{ + RemoteChainCfgs: map[uint64]seq1_6.FeeQuoterImportConfigSequenceOutputPerRemoteChain{ + 15971525489660198786: { + DestChainCfg: fee_quoter_v1_6_3.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxNumberOfTokensPerMsg: 3, + MaxDataBytes: 8000, + MaxPerMsgGasLimit: 4000000, + DestGasOverhead: 80000, + DestGasPerPayloadByteBase: 14, + DestGasPerPayloadByteHigh: 28, + DestGasPerPayloadByteThreshold: 800, + DestDataAvailabilityOverheadGas: 40000, + DestGasPerDataAvailabilityByte: 8, + DestDataAvailabilityMultiplierBps: 900, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(15971525489660198786)), + EnforceOutOfOrder: false, + DefaultTokenFeeUSDCents: 8, + DefaultTokenDestGasOverhead: 40000, + DefaultTxGasLimit: 180000, + GasMultiplierWeiPerEth: 0, + GasPriceStalenessThreshold: 0, + NetworkFeeUSDCents: 4, + }, + TokenTransferFeeCfgs: map[common.Address]fee_quoter_v1_6_3.FeeQuoterTokenTransferFeeConfig{ + common.HexToAddress("0x2222222222222222222222222222222222222222"): { + MinFeeUSDCents: 4, + MaxFeeUSDCents: 40, + DeciBps: 90, + DestGasOverhead: 25000, + DestBytesOverhead: 80, + IsEnabled: true, + }, + }, + }, + }, + StaticCfg: fee_quoter_v1_6_3.FeeQuoterStaticConfig{ + MaxFeeJuelsPerMsg: big.NewInt(1000000000000000000), + LinkToken: common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA"), + TokenPriceStalenessThreshold: 3600, + }, + PriceUpdaters: []common.Address{ + common.HexToAddress("0x4444444444444444444444444444444444444444"), + common.HexToAddress("0x5555555555555555555555555555555555555555"), + }, + }, + }, + { + Address: "0x1010101010101010101010101010101010101010", + ChainSelector: 4949039107694359620, + Metadata: seq1_5.OnRampImportConfigSequenceOutput{ + RemoteChainSelector: 5009297550715157269, + StaticConfig: evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampStaticConfig{ + LinkToken: common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA"), + ChainSelector: 4949039107694359620, + DestChainSelector: 5009297550715157269, + DefaultTxGasLimit: 200000, + MaxNopFeesJuels: big.NewInt(1000000000000000000), + PrevOnRamp: common.HexToAddress("0x0000000000000000000000000000000000000000"), + RmnProxy: common.HexToAddress("0x7777777777777777777777777777777777777777"), + TokenAdminRegistry: common.HexToAddress("0x8888888888888888888888888888888888888888"), + }, + DynamicConfig: evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampDynamicConfig{ + Router: common.HexToAddress("0x9999999999999999999999999999999999999999"), + MaxNumberOfTokensPerMsg: 5, + DestGasOverhead: 100000, + DestGasPerPayloadByte: 16, + DestDataAvailabilityOverheadGas: 50000, + DestGasPerDataAvailabilityByte: 10, + DestDataAvailabilityMultiplierBps: 1000, + PriceRegistry: common.HexToAddress("0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + MaxDataBytes: 10000, + MaxPerMsgGasLimit: 5000000, + DefaultTokenFeeUSDCents: 0, + DefaultTokenDestGasOverhead: 0, + EnforceOutOfOrder: false, + }, + TokenTransferFeeConfig: map[common.Address]evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampTokenTransferFeeConfig{ + common.HexToAddress("0x2222222222222222222222222222222222222222"): { + MinFeeUSDCents: 5, + MaxFeeUSDCents: 50, + DeciBps: 100, + DestGasOverhead: 30000, + DestBytesOverhead: 100, + AggregateRateLimitEnabled: false, + IsEnabled: true, + }, + }, + FeeTokenConfig: map[common.Address]evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampFeeTokenConfig{ + common.HexToAddress("0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"): { + NetworkFeeUSDCents: 5, + GasMultiplierWeiPerEth: 1000000000000000000, + PremiumMultiplierWeiPerEth: 1100000000000000000, + Enabled: true, + }, + }, + }, + }, + { + Address: "0x5050505050505050505050505050505050505050", + ChainSelector: 15971525489660198786, + Metadata: seq1_5.OnRampImportConfigSequenceOutput{ + RemoteChainSelector: 5009297550715157269, + StaticConfig: evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampStaticConfig{ + LinkToken: common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA"), + ChainSelector: 15971525489660198786, + DestChainSelector: 5009297550715157269, + DefaultTxGasLimit: 200000, + MaxNopFeesJuels: big.NewInt(1000000000000000000), + PrevOnRamp: common.HexToAddress("0x0000000000000000000000000000000000000000"), + RmnProxy: common.HexToAddress("0x7777777777777777777777777777777777777777"), + TokenAdminRegistry: common.HexToAddress("0x8888888888888888888888888888888888888888"), + }, + DynamicConfig: evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampDynamicConfig{ + Router: common.HexToAddress("0x9999999999999999999999999999999999999999"), + MaxNumberOfTokensPerMsg: 5, + DestGasOverhead: 100000, + DestGasPerPayloadByte: 16, + DestDataAvailabilityOverheadGas: 50000, + DestGasPerDataAvailabilityByte: 10, + DestDataAvailabilityMultiplierBps: 1000, + PriceRegistry: common.HexToAddress("0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + MaxDataBytes: 10000, + MaxPerMsgGasLimit: 5000000, + DefaultTokenFeeUSDCents: 0, + DefaultTokenDestGasOverhead: 0, + EnforceOutOfOrder: false, + }, + TokenTransferFeeConfig: map[common.Address]evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampTokenTransferFeeConfig{ + common.HexToAddress("0x2222222222222222222222222222222222222222"): { + MinFeeUSDCents: 5, + MaxFeeUSDCents: 50, + DeciBps: 100, + DestGasOverhead: 30000, + DestBytesOverhead: 100, + AggregateRateLimitEnabled: false, + IsEnabled: true, + }, + common.HexToAddress("0x3333333333333333333333333333333333333333"): { + MinFeeUSDCents: 10, + MaxFeeUSDCents: 100, + DeciBps: 200, + DestGasOverhead: 40000, + DestBytesOverhead: 200, + AggregateRateLimitEnabled: false, + IsEnabled: true, + }, + }, + FeeTokenConfig: map[common.Address]evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampFeeTokenConfig{ + common.HexToAddress("0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"): { + NetworkFeeUSDCents: 5, + GasMultiplierWeiPerEth: 1000000000000000000, + PremiumMultiplierWeiPerEth: 1100000000000000000, + Enabled: true, + }, + common.HexToAddress("0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"): { + NetworkFeeUSDCents: 3, + GasMultiplierWeiPerEth: 900000000000000000, + PremiumMultiplierWeiPerEth: 1000000000000000000, + Enabled: true, + }, + }, + }, + }, + { + Address: "0x6060606060606060606060606060606060606060", + ChainSelector: 5936861837188149645, + Metadata: seq1_6.FeeQuoterImportConfigSequenceOutput{ + RemoteChainCfgs: map[uint64]seq1_6.FeeQuoterImportConfigSequenceOutputPerRemoteChain{ + 5009297550715157269: { + DestChainCfg: fee_quoter_v1_6_3.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxNumberOfTokensPerMsg: 5, + MaxDataBytes: 10000, + MaxPerMsgGasLimit: 5000000, + DestGasOverhead: 100000, + DestGasPerPayloadByteBase: 16, + DestGasPerPayloadByteHigh: 32, + DestGasPerPayloadByteThreshold: 1000, + DestDataAvailabilityOverheadGas: 50000, + DestGasPerDataAvailabilityByte: 10, + DestDataAvailabilityMultiplierBps: 1000, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(5009297550715157269)), + EnforceOutOfOrder: false, + DefaultTokenFeeUSDCents: 10, + DefaultTokenDestGasOverhead: 50000, + DefaultTxGasLimit: 200000, + GasMultiplierWeiPerEth: 0, + GasPriceStalenessThreshold: 0, + NetworkFeeUSDCents: 5, + }, + TokenTransferFeeCfgs: map[common.Address]fee_quoter_v1_6_3.FeeQuoterTokenTransferFeeConfig{}, + }, + 4949039107694359620: { + DestChainCfg: fee_quoter_v1_6_3.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxNumberOfTokensPerMsg: 4, + MaxDataBytes: 9000, + MaxPerMsgGasLimit: 4500000, + DestGasOverhead: 90000, + DestGasPerPayloadByteBase: 15, + DestGasPerPayloadByteHigh: 30, + DestGasPerPayloadByteThreshold: 900, + DestDataAvailabilityOverheadGas: 45000, + DestGasPerDataAvailabilityByte: 9, + DestDataAvailabilityMultiplierBps: 950, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(4949039107694359620)), + EnforceOutOfOrder: false, + DefaultTokenFeeUSDCents: 9, + DefaultTokenDestGasOverhead: 45000, + DefaultTxGasLimit: 190000, + GasMultiplierWeiPerEth: 0, + GasPriceStalenessThreshold: 0, + NetworkFeeUSDCents: 4, + }, + TokenTransferFeeCfgs: map[common.Address]fee_quoter_v1_6_3.FeeQuoterTokenTransferFeeConfig{}, + }, + }, + StaticCfg: fee_quoter_v1_6_3.FeeQuoterStaticConfig{ + MaxFeeJuelsPerMsg: big.NewInt(1000000000000000000), + LinkToken: common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA"), + TokenPriceStalenessThreshold: 3600, + }, + PriceUpdaters: []common.Address{ + common.HexToAddress("0x4444444444444444444444444444444444444444"), + common.HexToAddress("0x5555555555555555555555555555555555555555"), + }, + }, + }, + { + Address: "0x7070707070707070707070707070707070707070", + ChainSelector: 5936861837188149645, + Metadata: seq1_5.OnRampImportConfigSequenceOutput{ + RemoteChainSelector: 15971525489660198786, + StaticConfig: evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampStaticConfig{ + LinkToken: common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA"), + ChainSelector: 5936861837188149645, + DestChainSelector: 15971525489660198786, + DefaultTxGasLimit: 180000, + MaxNopFeesJuels: big.NewInt(900000000000000000), + PrevOnRamp: common.HexToAddress("0x0000000000000000000000000000000000000000"), + RmnProxy: common.HexToAddress("0x7777777777777777777777777777777777777777"), + TokenAdminRegistry: common.HexToAddress("0x8888888888888888888888888888888888888888"), + }, + DynamicConfig: evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampDynamicConfig{ + Router: common.HexToAddress("0x9999999999999999999999999999999999999999"), + MaxNumberOfTokensPerMsg: 3, + DestGasOverhead: 80000, + DestGasPerPayloadByte: 14, + DestDataAvailabilityOverheadGas: 40000, + DestGasPerDataAvailabilityByte: 8, + DestDataAvailabilityMultiplierBps: 900, + PriceRegistry: common.HexToAddress("0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + MaxDataBytes: 8000, + MaxPerMsgGasLimit: 4000000, + DefaultTokenFeeUSDCents: 0, + DefaultTokenDestGasOverhead: 0, + EnforceOutOfOrder: false, + }, + TokenTransferFeeConfig: map[common.Address]evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampTokenTransferFeeConfig{}, + FeeTokenConfig: map[common.Address]evm_2_evm_onramp_v1_5_0.EVM2EVMOnRampFeeTokenConfig{ + common.HexToAddress("0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"): { + NetworkFeeUSDCents: 4, + GasMultiplierWeiPerEth: 950000000000000000, + PremiumMultiplierWeiPerEth: 1050000000000000000, + Enabled: true, + }, + }, + }, + }, +} + +// getExpectedOutput returns hardcoded expected FeeQuoterUpdate values based on contract_metadata.json +func getExpectedOutput() map[uint64]sequence1_7.FeeQuoterUpdate { + linkToken := common.HexToAddress("0x514910771AF9Ca656af840dff83E8264EcF986CA") + maxFeeJuels, _ := new(big.Int).SetString("1000000000000000000", 10) + priceUpdater1 := common.HexToAddress("0x4444444444444444444444444444444444444444") + priceUpdater2 := common.HexToAddress("0x5555555555555555555555555555555555555555") + + expected := make(map[uint64]sequence1_7.FeeQuoterUpdate) + + // Chain 5009297550715157269: Has FeeQuoter v1.6.3 + OnRamp v1.5.0 + // Since no FeeQuoter v1.7.0 exists, it's a new deployment (ConstructorArgs populated) + expected[5009297550715157269] = sequence1_7.FeeQuoterUpdate{ + ChainSelector: 5009297550715157269, + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: linkToken, + MaxFeeJuelsPerMsg: maxFeeJuels, + }, + PriceUpdaters: []common.Address{priceUpdater1, priceUpdater2}, + DestChainConfigArgs: []fqops.DestChainConfigArgs{ + { + DestChainSelector: 15971525489660198786, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 8000, + MaxPerMsgGasLimit: 4000000, + DestGasOverhead: 80000, + DestGasPerPayloadByteBase: 14, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(15971525489660198786)), + DefaultTokenFeeUSDCents: 8, + DefaultTokenDestGasOverhead: 40000, + DefaultTxGasLimit: 180000, + NetworkFeeUSDCents: 4, + LinkFeeMultiplierPercent: 90, + }, + }, + { + DestChainSelector: 4949039107694359620, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 10000, + MaxPerMsgGasLimit: 5000000, + DestGasOverhead: 100000, + DestGasPerPayloadByteBase: 16, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(4949039107694359620)), + DefaultTokenFeeUSDCents: 0, // Not in OnRamp DynamicConfig in test data + DefaultTokenDestGasOverhead: 0, // Not in OnRamp DynamicConfig in test data + DefaultTxGasLimit: 200000, + NetworkFeeUSDCents: 10, + LinkFeeMultiplierPercent: 90, + }, + }, + }, + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + { + DestChainSelector: 15971525489660198786, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + { + Token: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenTransferFeeConfig: fee_quoter.FeeQuoterTokenTransferFeeConfig{ + FeeUSDCents: 4, + DestGasOverhead: 25000, + DestBytesOverhead: 80, + IsEnabled: true, + }, + }, + }, + }, + { + DestChainSelector: 4949039107694359620, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + { + Token: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenTransferFeeConfig: fee_quoter.FeeQuoterTokenTransferFeeConfig{ + FeeUSDCents: 5, + DestGasOverhead: 30000, + DestBytesOverhead: 100, + IsEnabled: true, + }, + }, + { + Token: common.HexToAddress("0x3333333333333333333333333333333333333333"), + TokenTransferFeeConfig: fee_quoter.FeeQuoterTokenTransferFeeConfig{ + FeeUSDCents: 10, + DestGasOverhead: 40000, + DestBytesOverhead: 200, + IsEnabled: true, + }, + }, + }, + }, + }, + }, + } + + // Chain 4949039107694359620: Has FeeQuoter v1.6.3 + OnRamp v1.5.0 + expected[4949039107694359620] = sequence1_7.FeeQuoterUpdate{ + ChainSelector: 4949039107694359620, + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: linkToken, + MaxFeeJuelsPerMsg: maxFeeJuels, + }, + PriceUpdaters: []common.Address{priceUpdater1, priceUpdater2}, + DestChainConfigArgs: []fqops.DestChainConfigArgs{ + { + DestChainSelector: 15971525489660198786, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 8000, + MaxPerMsgGasLimit: 4000000, + DestGasOverhead: 80000, + DestGasPerPayloadByteBase: 14, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(15971525489660198786)), + DefaultTokenFeeUSDCents: 8, + DefaultTokenDestGasOverhead: 40000, + DefaultTxGasLimit: 180000, + NetworkFeeUSDCents: 4, + LinkFeeMultiplierPercent: 90, + }, + }, + { + DestChainSelector: 5009297550715157269, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 10000, + MaxPerMsgGasLimit: 5000000, + DestGasOverhead: 100000, + DestGasPerPayloadByteBase: 16, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(5009297550715157269)), + DefaultTokenFeeUSDCents: 0, // Not in OnRamp DynamicConfig in test data + DefaultTokenDestGasOverhead: 0, // Not in OnRamp DynamicConfig in test data + DefaultTxGasLimit: 200000, + NetworkFeeUSDCents: 10, + LinkFeeMultiplierPercent: 90, + }, + }, + }, + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + { + DestChainSelector: 15971525489660198786, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + { + Token: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenTransferFeeConfig: fee_quoter.FeeQuoterTokenTransferFeeConfig{ + FeeUSDCents: 4, + DestGasOverhead: 25000, + DestBytesOverhead: 80, + IsEnabled: true, + }, + }, + }, + }, + { + DestChainSelector: 5009297550715157269, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + { + Token: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenTransferFeeConfig: fee_quoter.FeeQuoterTokenTransferFeeConfig{ + FeeUSDCents: 5, + DestGasOverhead: 30000, + DestBytesOverhead: 100, + IsEnabled: true, + }, + }, + }, + }, + }, + }, + } + + // Chain 15971525489660198786: Only has OnRamp v1.5.0 + maxFeeJuels159, _ := new(big.Int).SetString("1000000000000000000", 10) + expected[15971525489660198786] = sequence1_7.FeeQuoterUpdate{ + ChainSelector: 15971525489660198786, + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: linkToken, + MaxFeeJuelsPerMsg: maxFeeJuels159, + }, + DestChainConfigArgs: []fqops.DestChainConfigArgs{ + { + DestChainSelector: 5009297550715157269, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 10000, + MaxPerMsgGasLimit: 5000000, + DestGasOverhead: 100000, + DestGasPerPayloadByteBase: 16, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(5009297550715157269)), + DefaultTokenFeeUSDCents: 0, // Not in OnRamp DynamicConfig in test data + DefaultTokenDestGasOverhead: 0, // Not in OnRamp DynamicConfig in test data + DefaultTxGasLimit: 200000, + NetworkFeeUSDCents: 10, + LinkFeeMultiplierPercent: 90, + }, + }, + }, + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + { + DestChainSelector: 5009297550715157269, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{ + { + Token: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenTransferFeeConfig: fee_quoter.FeeQuoterTokenTransferFeeConfig{ + FeeUSDCents: 5, + DestGasOverhead: 30000, + DestBytesOverhead: 100, + IsEnabled: true, + }, + }, + { + Token: common.HexToAddress("0x3333333333333333333333333333333333333333"), + TokenTransferFeeConfig: fee_quoter.FeeQuoterTokenTransferFeeConfig{ + FeeUSDCents: 10, + DestGasOverhead: 40000, + DestBytesOverhead: 200, + IsEnabled: true, + }, + }, + }, + }, + }, + }, + } + + // Chain 5936861837188149645: Has FeeQuoter v1.6.3 + OnRamp v1.5.0 + expected[5936861837188149645] = sequence1_7.FeeQuoterUpdate{ + ChainSelector: 5936861837188149645, + ConstructorArgs: fqops.ConstructorArgs{ + StaticConfig: fqops.StaticConfig{ + LinkToken: linkToken, + MaxFeeJuelsPerMsg: maxFeeJuels, + }, + PriceUpdaters: []common.Address{priceUpdater1, priceUpdater2}, + DestChainConfigArgs: []fqops.DestChainConfigArgs{ + { + DestChainSelector: 5009297550715157269, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 10000, + MaxPerMsgGasLimit: 5000000, + DestGasOverhead: 100000, + DestGasPerPayloadByteBase: 16, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(5009297550715157269)), + DefaultTokenFeeUSDCents: 10, + DefaultTokenDestGasOverhead: 50000, + DefaultTxGasLimit: 200000, + NetworkFeeUSDCents: 5, + LinkFeeMultiplierPercent: 90, + }, + }, + { + DestChainSelector: 4949039107694359620, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 9000, + MaxPerMsgGasLimit: 4500000, + DestGasOverhead: 90000, + DestGasPerPayloadByteBase: 15, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(4949039107694359620)), + DefaultTokenFeeUSDCents: 9, + DefaultTokenDestGasOverhead: 45000, + DefaultTxGasLimit: 190000, + NetworkFeeUSDCents: 4, + LinkFeeMultiplierPercent: 90, + }, + }, + { + DestChainSelector: 15971525489660198786, + DestChainConfig: adapters.FeeQuoterDestChainConfig{ + IsEnabled: true, + MaxDataBytes: 8000, + MaxPerMsgGasLimit: 4000000, + DestGasOverhead: 80000, + DestGasPerPayloadByteBase: 14, + ChainFamilySelector: [4]byte(utils.GetSelectorHex(15971525489660198786)), + DefaultTokenFeeUSDCents: 0, // Not in OnRamp DynamicConfig in test data + DefaultTokenDestGasOverhead: 0, // Not in OnRamp DynamicConfig in test data + DefaultTxGasLimit: 180000, + NetworkFeeUSDCents: 10, + LinkFeeMultiplierPercent: 90, + }, + }, + }, + TokenTransferFeeConfigArgs: []fqops.TokenTransferFeeConfigArgs{ + { + DestChainSelector: 5009297550715157269, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{}, + }, + { + DestChainSelector: 4949039107694359620, + TokenTransferFeeConfigs: []fee_quoter.FeeQuoterTokenTransferFeeConfigSingleTokenArgs{}, + }, + }, + }, + } + + return expected +} + +func TestSequenceFeeQuoterInputCreation(t *testing.T) { + contractMetadata := dummyContractMetadata + addressRefs := dummyAddressRefs + + // Collect unique chain selectors from address refs + chainSelectors := make(map[uint64]bool) + for _, ref := range addressRefs { + chainSelectors[ref.ChainSelector] = true + } + + // Convert map keys to slice + chainSelectorList := make([]uint64, 0, len(chainSelectors)) + for selector := range chainSelectors { + chainSelectorList = append(chainSelectorList, selector) + } + + // Create environment with simulated EVM chains + e, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, chainSelectorList), + ) + require.NoError(t, err, "Failed to create environment") + require.NotNil(t, e, "Environment should be created") + + // Load address refs into a new datastore + // Note: The environment's datastore is sealed, so we'll use our own datastore + // and pass the data directly in the input to the sequence + ds := datastore.NewMemoryDataStore() + for _, ref := range addressRefs { + err := ds.Addresses().Add(ref) + require.NoError(t, err, "Failed to add address ref to datastore") + } + + // Load contract metadata into the datastore + err = sequences.WriteMetadataToDatastore(ds, sequences.Metadata{ + Contracts: contractMetadata, + }) + require.NoError(t, err, "Failed to write contract metadata to datastore") + + // Seal the datastore for use in the test + e.DataStore = ds.Seal() + + // Get the FeeQuoterUpdater adapter (use concrete type so report.Output is sequence1_7.FeeQuoterUpdate) + fquUpdater := evmadapter.FeeQuoterUpdater[sequence1_7.FeeQuoterUpdate]{} + + // Test the sequence for each chain selector that has a FeeQuoter + for _, chainSelector := range chainSelectorList { + _, ok := e.BlockChains.EVMChains()[chainSelector] + require.True(t, ok, "Chain with selector %d should exist", chainSelector) + // Filter existing addresses for this chain + existingAddresses := e.DataStore.Addresses().Filter(datastore.AddressRefByChainSelector(chainSelector)) + + // Create input for SequenceFeeQuoterInputCreation + input := deploy.FeeQuoterUpdateInput{ + ChainSelector: chainSelector, + ExistingAddresses: existingAddresses, + ContractMeta: e.DataStore.ContractMetadata().Filter(datastore.ContractMetadataByChainSelector(chainSelector)), + } + + // Execute the sequence + report, err := cldf_ops.ExecuteSequence( + e.OperationsBundle, + fquUpdater.SequenceFeeQuoterInputCreation(), + e.BlockChains, + input, + ) + + // Verify the sequence executed successfully + require.NoError(t, err, "SequenceFeeQuoterInputCreation should not error for chain %d", chainSelector) + require.NotNil(t, report, "Report should not be nil for chain %d", chainSelector) + + // Verify the output is not empty + output := report.Output + isEmpty, err := output.IsEmpty() + require.NoError(t, err, "IsEmpty check should not error") + require.False(t, isEmpty, "Output should not be empty for chain %d", chainSelector) + + // Verify basic output structure + require.Equal(t, chainSelector, output.ChainSelector, "Chain selector should match input") + require.Equal(t, existingAddresses, output.ExistingAddresses, "Existing addresses should match input") + + // Get expected output (hardcoded based on contract_metadata.json) + expectedMap := getExpectedOutput() + expected, hasExpected := expectedMap[chainSelector] + require.True(t, hasExpected, "Expected output should exist for chain %d", chainSelector) + + // Verify that the output has meaningful data + // At least one of these should be populated: + // - ConstructorArgs + // - DestChainConfigs + // - TokenTransferFeeConfigUpdates + // - AuthorizedCallerUpdates + hasData := !output.ConstructorArgs.IsEmpty() || + len(output.DestChainConfigs) > 0 || + len(output.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs) > 0 || + len(output.AuthorizedCallerUpdates.AddedCallers) > 0 || + len(output.AuthorizedCallerUpdates.RemovedCallers) > 0 + + require.True(t, hasData, "Output should have at least some configuration data for chain %d", chainSelector) + // Assert against expected values + if !expected.ConstructorArgs.IsEmpty() { + require.False(t, output.ConstructorArgs.IsEmpty(), "ConstructorArgs should be present for new deployment on chain %d", chainSelector) + require.Equal(t, expected.ConstructorArgs.StaticConfig.LinkToken, output.ConstructorArgs.StaticConfig.LinkToken, + "LinkToken should match expected value on chain %d", chainSelector) + require.Equal(t, 0, expected.ConstructorArgs.StaticConfig.MaxFeeJuelsPerMsg.Cmp(output.ConstructorArgs.StaticConfig.MaxFeeJuelsPerMsg), + "MaxFeeJuelsPerMsg should match expected value on chain %d", chainSelector) + require.ElementsMatch(t, expected.ConstructorArgs.PriceUpdaters, output.ConstructorArgs.PriceUpdaters, + "PriceUpdaters should match expected value on chain %d", chainSelector) + } else { + // For existing deployments, ConstructorArgs should be empty + require.True(t, output.ConstructorArgs.IsEmpty(), "ConstructorArgs should be empty for existing deployment on chain %d", chainSelector) + } + + // Assert specific values based on the sequence logic in feequoterupdater.go + // The sequence merges outputs from CreateFeeQuoterUpdateInputFromV163 and CreateFeeQuoterUpdateInputFromV150 + + // Verify DestChainConfigs against expected values + // Build a map of expected dest chain configs for easier lookup + expectedDestChainConfigsMap := make(map[uint64]fqops.DestChainConfigArgs) + for _, cfg := range expected.DestChainConfigs { + expectedDestChainConfigsMap[cfg.DestChainSelector] = cfg + } + require.Len(t, output.DestChainConfigs, len(expectedDestChainConfigsMap), + "Number of DestChainConfigs should match expected value on chain %d", chainSelector) + + for _, destChainCfg := range output.DestChainConfigs { + if expectedCfg, exists := expectedDestChainConfigsMap[destChainCfg.DestChainSelector]; exists { + require.Equal(t, expectedCfg, destChainCfg, "DestChainConfig should match expected value for "+ + "DestChainSelector %d on chain %d", destChainCfg.DestChainSelector, chainSelector) + } + } + for _, cfg := range expected.ConstructorArgs.DestChainConfigArgs { + expectedDestChainConfigsMap[cfg.DestChainSelector] = cfg + } + require.Len(t, output.ConstructorArgs.DestChainConfigArgs, len(expectedDestChainConfigsMap), + "Number of Constructor DestChainConfigArgs should match expected value for chain %d", chainSelector) + + for _, destChainCfg := range output.ConstructorArgs.DestChainConfigArgs { + if expectedCfg, exists := expectedDestChainConfigsMap[destChainCfg.DestChainSelector]; exists { + require.Equal(t, expectedCfg, destChainCfg, "Constructor DestChainConfig should match expected value for "+ + "DestChainSelector %d on chain %d", destChainCfg.DestChainSelector, chainSelector) + } + } + + require.Len(t, output.ConstructorArgs.TokenTransferFeeConfigArgs, len(expected.ConstructorArgs.TokenTransferFeeConfigArgs), + "Number of TokenTransferFeeConfigArgs should match expected value for chain %d", chainSelector) + require.Len(t, output.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs, len(expected.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs), + "Number of TokenTransferFeeConfigUpdates should match expected value for chain %d", chainSelector) + for _, tokenTransferFeeConfig := range output.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs { + found := false + for _, expectedCfg := range expected.TokenTransferFeeConfigUpdates.TokenTransferFeeConfigArgs { + if tokenTransferFeeConfig.DestChainSelector == expectedCfg.DestChainSelector { + require.ElementsMatch(t, expectedCfg.TokenTransferFeeConfigs, tokenTransferFeeConfig.TokenTransferFeeConfigs, + "TokenTransferFeeConfigs should match expected value for DestChainSelector %d on chain %d", + tokenTransferFeeConfig.DestChainSelector, chainSelector) + found = true + break + } + } + require.True(t, found, "Unexpected TokenTransferFeeConfig for DestChainSelector %d on chain %d", + tokenTransferFeeConfig.DestChainSelector, chainSelector) + } + + for _, tokenTransferFeeConfig := range output.ConstructorArgs.TokenTransferFeeConfigArgs { + found := false + for _, expectedCfg := range expected.ConstructorArgs.TokenTransferFeeConfigArgs { + if tokenTransferFeeConfig.DestChainSelector == expectedCfg.DestChainSelector { + require.ElementsMatch(t, expectedCfg.TokenTransferFeeConfigs, tokenTransferFeeConfig.TokenTransferFeeConfigs, + "Constructor TokenTransferFeeConfigs should match expected value for DestChainSelector %d on chain %d", + tokenTransferFeeConfig.DestChainSelector, chainSelector) + found = true + break + } + } + require.True(t, found, "Unexpected Constructor TokenTransferFeeConfig for DestChainSelector %d on chain %d", + tokenTransferFeeConfig.DestChainSelector, chainSelector) + } + + // Verify AuthorizedCallerUpdates if present (for existing deployments) + require.ElementsMatch(t, expected.AuthorizedCallerUpdates.AddedCallers, output.AuthorizedCallerUpdates.AddedCallers, + "AuthorizedCallerUpdates.AddedCallers should match expected value on chain %d", chainSelector) + require.ElementsMatch(t, expected.AuthorizedCallerUpdates.RemovedCallers, output.AuthorizedCallerUpdates.RemovedCallers, + "AuthorizedCallerUpdates.RemovedCallers should match expected value on chain %d", chainSelector) + + t.Logf("Successfully executed SequenceFeeQuoterInputCreation for chain %d", chainSelector) + } +} diff --git a/chains/evm/deployment/v1_2_0/operations/price_registry/price_registry.go b/chains/evm/deployment/v1_2_0/operations/price_registry/price_registry.go new file mode 100644 index 0000000000..ea1d7c6a36 --- /dev/null +++ b/chains/evm/deployment/v1_2_0/operations/price_registry/price_registry.go @@ -0,0 +1,27 @@ +package price_registry + +import ( + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/price_registry" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract" +) + +var ( + ContractType = cldf.ContractType("PriceRegistry") + Version = semver.MustParse("1.2.0") +) + +var PriceRegistryGetFeeToken = contract.NewRead(contract.ReadParams[any, []common.Address, *price_registry.PriceRegistry]{ + Name: "price_registry:getfeetokens", + Version: Version, + Description: "gets fee token from price registry 1.2", + ContractType: ContractType, + NewContract: price_registry.NewPriceRegistry, + CallContract: func(pr *price_registry.PriceRegistry, opts *bind.CallOpts, args any) ([]common.Address, error) { + return pr.GetFeeTokens(opts) + }, +}) diff --git a/chains/evm/deployment/v1_5_0/adapters/configimport.go b/chains/evm/deployment/v1_5_0/adapters/configimport.go index 6f2b02d16e..4745a23d2f 100644 --- a/chains/evm/deployment/v1_5_0/adapters/configimport.go +++ b/chains/evm/deployment/v1_5_0/adapters/configimport.go @@ -5,21 +5,24 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/router" cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/evm_2_evm_offramp" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/evm_2_evm_onramp" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/token_admin_registry" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_1/token_pool" + evm_datastore_utils "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/datastore" + priceregistryops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/price_registry" offrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/offramp" onrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/onramp" tokenadminops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/token_admin_registry" seq1_5 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/sequences" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/evm_2_evm_offramp" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/evm_2_evm_onramp" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/token_admin_registry" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_1/token_pool" api "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore" "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" @@ -28,77 +31,104 @@ import ( var GetTokensPaginationSize = uint64(20) type ConfigImportAdapter struct { - OnRamp map[uint64]map[uint64]common.Address - OffRamp map[uint64]map[uint64]common.Address - TokenAdminReg map[uint64]common.Address + OnRamp map[uint64]common.Address + OffRamp map[uint64]common.Address + TokenAdminReg common.Address + PriceRegistry common.Address + Router common.Address } -func (ci *ConfigImportAdapter) InitializeAdapter(e cldf.Environment, selectors []uint64) error { - ci.OnRamp = make(map[uint64]map[uint64]common.Address) - ci.OffRamp = make(map[uint64]map[uint64]common.Address) - ci.TokenAdminReg = make(map[uint64]common.Address) - for _, sel := range selectors { - ci.OnRamp[sel] = make(map[uint64]common.Address) - onRampRefs := e.DataStore.Addresses().Filter( - datastore.AddressRefByType(datastore.ContractType(onrampops.ContractType)), - datastore.AddressRefByVersion(onrampops.Version), - datastore.AddressRefByChainSelector(sel), - ) +func (ci *ConfigImportAdapter) InitializeAdapter(e cldf.Environment, sel uint64) error { + ci.OnRamp = make(map[uint64]common.Address) + onRampRefs := e.DataStore.Addresses().Filter( + datastore.AddressRefByType(datastore.ContractType(onrampops.ContractType)), + datastore.AddressRefByVersion(onrampops.Version), + datastore.AddressRefByChainSelector(sel), + ) - if len(onRampRefs) == 0 { - return fmt.Errorf("failed to get onramp ref for chain %d", sel) - } - chain := e.BlockChains.EVMChains()[sel] - for _, ref := range onRampRefs { - onRampC, err := evm_2_evm_onramp.NewEVM2EVMOnRamp(common.HexToAddress(ref.Address), chain.Client) - if err != nil { - return fmt.Errorf("failed to instantiate onramp contract for chain %d: %w", sel, err) - } - staticCfg, err := onRampC.GetStaticConfig(nil) - if err != nil { - return err - } - ci.OnRamp[sel][staticCfg.DestChainSelector] = common.HexToAddress(ref.Address) + if len(onRampRefs) == 0 { + return fmt.Errorf("failed to get onramp ref for chain %d", sel) + } + chain := e.BlockChains.EVMChains()[sel] + for _, ref := range onRampRefs { + onRampC, err := evm_2_evm_onramp.NewEVM2EVMOnRamp(common.HexToAddress(ref.Address), chain.Client) + if err != nil { + return fmt.Errorf("failed to instantiate onramp contract for chain %d: %w", sel, err) } - offRampRefs := e.DataStore.Addresses().Filter( - datastore.AddressRefByType(datastore.ContractType(offrampops.ContractType)), - datastore.AddressRefByVersion(offrampops.Version), - datastore.AddressRefByChainSelector(sel), - ) - if len(offRampRefs) == 0 { - return fmt.Errorf("failed to get offramp ref for chain %d", sel) + staticCfg, err := onRampC.GetStaticConfig(nil) + if err != nil { + return err } - for _, ref := range offRampRefs { - offRampC, err := evm_2_evm_offramp.NewEVM2EVMOffRamp(common.HexToAddress(ref.Address), chain.Client) - if err != nil { - return fmt.Errorf("failed to instantiate offramp contract for chain %d: %w", sel, err) - } - staticCfg, err := offRampC.GetStaticConfig(nil) - if err != nil { - return err - } - ci.OffRamp[sel][staticCfg.SourceChainSelector] = common.HexToAddress(ref.Address) + ci.OnRamp[staticCfg.DestChainSelector] = common.HexToAddress(ref.Address) + } + offRampRefs := e.DataStore.Addresses().Filter( + datastore.AddressRefByType(datastore.ContractType(offrampops.ContractType)), + datastore.AddressRefByVersion(offrampops.Version), + datastore.AddressRefByChainSelector(sel), + ) + if len(offRampRefs) == 0 { + return fmt.Errorf("failed to get offramp ref for chain %d", sel) + } + for _, ref := range offRampRefs { + offRampC, err := evm_2_evm_offramp.NewEVM2EVMOffRamp(common.HexToAddress(ref.Address), chain.Client) + if err != nil { + return fmt.Errorf("failed to instantiate offramp contract for chain %d: %w", sel, err) } - tokenAdminRegRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ - Type: datastore.ContractType(tokenadminops.ContractType), - Version: tokenadminops.Version, - }, sel, evm_datastore_utils.ToEVMAddress) + staticCfg, err := offRampC.GetStaticConfig(nil) if err != nil { - return fmt.Errorf("failed to find token admin registry contract ref for chain %d: %w", sel, err) + return err } - ci.TokenAdminReg[sel] = tokenAdminRegRef + ci.OffRamp[staticCfg.SourceChainSelector] = common.HexToAddress(ref.Address) + } + tokenAdminRegRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ + Type: datastore.ContractType(tokenadminops.ContractType), + Version: tokenadminops.Version, + }, sel, evm_datastore_utils.ToEVMAddress) + if err != nil { + return fmt.Errorf("failed to find token admin registry contract ref for chain %d: %w", sel, err) } + ci.TokenAdminReg = tokenAdminRegRef + priceRegistryRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ + Type: datastore.ContractType(priceregistryops.ContractType), + Version: priceregistryops.Version, + }, sel, evm_datastore_utils.ToEVMAddress) + if err != nil { + return fmt.Errorf("failed to find price registry contract ref for chain %d: %w", sel, err) + } + ci.PriceRegistry = priceRegistryRef + routerRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ + Type: datastore.ContractType("Router"), + Version: semver.MustParse("1.2.0"), + }, sel, evm_datastore_utils.ToEVMAddress) + if err != nil { + return fmt.Errorf("failed to find router contract ref for chain %d: %w", sel, err) + } + ci.Router = routerRef return nil } -func (ci *ConfigImportAdapter) ConnectedChains(_ cldf.Environment, chainsel uint64) ([]uint64, error) { - onRamps, ok := ci.OnRamp[chainsel] +func (ci *ConfigImportAdapter) ConnectedChains(e cldf.Environment, chainsel uint64) ([]uint64, error) { + var connected []uint64 + // to ensure deduplication in case there are multiple onramps addresses in datastore for the same remote chain selector + var mapConnectedChains = make(map[uint64]bool) + chain, ok := e.BlockChains.EVMChains()[chainsel] if !ok { - return nil, fmt.Errorf("no onramps found for chain %d", chainsel) + return nil, fmt.Errorf("chain with selector %d not found in environment", chainsel) } - var connected []uint64 - for destSel := range onRamps { - connected = append(connected, destSel) + routerC, err := router.NewRouter(ci.Router, chain.Client) + if err != nil { + return nil, fmt.Errorf("failed to instantiate router contract at %s on chain %d: %w", ci.Router.String(), chain.Selector, err) + } + for destSel, onrampForDest := range ci.OnRamp { + onRamp, err := routerC.GetOnRamp(nil, destSel) + if err != nil { + return nil, fmt.Errorf("failed to get onramp for dest chain %d from router at %s on chain %d: %w", destSel, ci.Router.String(), chain.Selector, err) + } + // if the onramp address from the router doesn't match the onramp address we have, then this chain is not actually connected with 1.5 + if onRamp == onrampForDest && !mapConnectedChains[destSel] { + connected = append(connected, destSel) + mapConnectedChains[destSel] = true + } } return connected, nil } @@ -108,12 +138,8 @@ func (ci *ConfigImportAdapter) SupportedTokensPerRemoteChain(e cldf.Environment, if !ok { return nil, fmt.Errorf("chain with selector %d not found in environment", chainsel) } - tokenAdminRegAddr, ok := ci.TokenAdminReg[chainsel] - if !ok { - return nil, fmt.Errorf("token admin registry address not found for chain %d", chainsel) - } // get all supported tokens from token admin registry - return GetSupportedTokensPerRemoteChain(tokenAdminRegAddr, chain) + return GetSupportedTokensPerRemoteChain(ci.TokenAdminReg, chain) } func (ci *ConfigImportAdapter) SequenceImportConfig() *cldf_ops.Sequence[api.ImportConfigPerChainInput, sequences.OnChainOutput, cldf_chain.BlockChains] { @@ -133,8 +159,9 @@ func (ci *ConfigImportAdapter) SequenceImportConfig() *cldf_ops.Sequence[api.Imp seq1_5.OnRampImportConfigSequence, seq1_5.OnRampImportConfigSequenceInput{ ChainSelector: chainSelector, - OnRampsPerRemoteChain: ci.OnRamp[chainSelector], + OnRampsPerRemoteChain: ci.OnRamp, SupportedTokensPerChain: in.TokensPerRemoteChain, + PriceRegistry: ci.PriceRegistry, }, result) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to import onramp config for chain %d: %w", chainSelector, err) @@ -143,7 +170,7 @@ func (ci *ConfigImportAdapter) SequenceImportConfig() *cldf_ops.Sequence[api.Imp seq1_5.OffRampImportConfigSequence, seq1_5.OffRampImportConfigSequenceInput{ ChainSelector: chainSelector, - OffRampsPerRemoteChain: ci.OffRamp[chainSelector], + OffRampsPerRemoteChain: ci.OffRamp, }, result) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to import offramp config for chain %d: %w", chainSelector, err) diff --git a/chains/evm/deployment/v1_5_0/operations/onramp/onramp.go b/chains/evm/deployment/v1_5_0/operations/onramp/onramp.go index d33c23990d..0fbba7ff1c 100644 --- a/chains/evm/deployment/v1_5_0/operations/onramp/onramp.go +++ b/chains/evm/deployment/v1_5_0/operations/onramp/onramp.go @@ -69,3 +69,14 @@ var OnRampDynamicConfig = contract.NewRead(contract.ReadParams[any, evm_2_evm_on return onRamp.GetDynamicConfig(opts) }, }) + +var OnRampFeeTokenConfig = contract.NewRead(contract.ReadParams[common.Address, evm_2_evm_onramp.EVM2EVMOnRampFeeTokenConfig, *evm_2_evm_onramp.EVM2EVMOnRamp]{ + Name: "onramp:fee-token-config", + Version: Version, + Description: "Reads the fee token config for a given token from the OnRamp 1.5.0 contract", + ContractType: ContractType, + NewContract: evm_2_evm_onramp.NewEVM2EVMOnRamp, + CallContract: func(onRamp *evm_2_evm_onramp.EVM2EVMOnRamp, opts *bind.CallOpts, args common.Address) (evm_2_evm_onramp.EVM2EVMOnRampFeeTokenConfig, error) { + return onRamp.GetFeeTokenConfig(opts, args) + }, +}) diff --git a/chains/evm/deployment/v1_5_0/sequences/onramp.go b/chains/evm/deployment/v1_5_0/sequences/onramp.go index b3774f2d03..bd68ad82f3 100644 --- a/chains/evm/deployment/v1_5_0/sequences/onramp.go +++ b/chains/evm/deployment/v1_5_0/sequences/onramp.go @@ -11,9 +11,11 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/operations" mcms_types "github.com/smartcontractkit/mcms/types" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/evm_2_evm_onramp" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract" + priceregistryops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/price_registry" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/onramp" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" ) @@ -25,6 +27,7 @@ type OnRampSetTokenTransferFeeConfigSequenceInput struct { type OnRampImportConfigSequenceInput struct { ChainSelector uint64 + PriceRegistry common.Address OnRampsPerRemoteChain map[uint64]common.Address SupportedTokensPerChain map[uint64][]common.Address } @@ -34,6 +37,7 @@ type OnRampImportConfigSequenceOutput struct { TokenTransferFeeConfig map[common.Address]evm_2_evm_onramp.EVM2EVMOnRampTokenTransferFeeConfig StaticConfig evm_2_evm_onramp.EVM2EVMOnRampStaticConfig DynamicConfig evm_2_evm_onramp.EVM2EVMOnRampDynamicConfig + FeeTokenConfig map[common.Address]evm_2_evm_onramp.EVM2EVMOnRampFeeTokenConfig } var ( @@ -92,6 +96,31 @@ var ( return sequences.OnChainOutput{}, fmt.Errorf("failed to execute OnRampDynamicConfigOp "+ "on %s for remote chain %d: %w", chain.String(), remoteChainSelector, err) } + feetokenOut, err := operations.ExecuteOperation(b, priceregistryops.PriceRegistryGetFeeToken, chain, contract.FunctionInput[any]{ + ChainSelector: chain.Selector, + Address: input.PriceRegistry, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to execute PriceRegistryGetFeeTokenOp "+ + "on %s for price registry %s: %w", chain.String(), input.PriceRegistry.String(), err) + } + feeTokens := feetokenOut.Output + feeTokenConfig := make(map[common.Address]evm_2_evm_onramp.EVM2EVMOnRampFeeTokenConfig) + for _, token := range feeTokens { + feeTokenConfigOut, err := operations.ExecuteOperation(b, onramp.OnRampFeeTokenConfig, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chain.Selector, + Address: onRampAddress, + Args: token, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to execute OnRampFeeTokenConfigOp "+ + "on %s for remote chain %d and token %s: %w", chain.String(), remoteChainSelector, token.String(), err) + } + if !feeTokenConfigOut.Output.Enabled { + continue + } + feeTokenConfig[token] = feeTokenConfigOut.Output + } tokenTransferFeeConfig := make(map[common.Address]evm_2_evm_onramp.EVM2EVMOnRampTokenTransferFeeConfig) for _, token := range input.SupportedTokensPerChain[remoteChainSelector] { ttfcOut, err := operations.ExecuteOperation(b, onramp.OnRampGetTokenTransferFeeConfig, chain, contract.FunctionInput[common.Address]{ @@ -103,6 +132,9 @@ var ( return sequences.OnChainOutput{}, fmt.Errorf("failed to execute OnRampGetTokenTransferFeeConfigOp "+ "on %s for remote chain %d and token %s: %w", chain.String(), remoteChainSelector, token.String(), err) } + if !ttfcOut.Output.IsEnabled { + continue + } tokenTransferFeeConfig[token] = ttfcOut.Output } contractMeta = append(contractMeta, datastore.ContractMetadata{ @@ -113,6 +145,7 @@ var ( StaticConfig: sCfgOut.Output, DynamicConfig: dCfgOut.Output, TokenTransferFeeConfig: tokenTransferFeeConfig, + FeeTokenConfig: feeTokenConfig, }, }) } diff --git a/chains/evm/deployment/v1_6_0/adapters/configimport.go b/chains/evm/deployment/v1_6_0/adapters/configimport.go index 3dcbc7a986..9926db699e 100644 --- a/chains/evm/deployment/v1_6_0/adapters/configimport.go +++ b/chains/evm/deployment/v1_6_0/adapters/configimport.go @@ -11,6 +11,8 @@ import ( cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/router" + evm_datastore_utils "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/datastore" adapters1_5 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/adapters" tokenadminops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/token_admin_registry" @@ -18,71 +20,63 @@ import ( offrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/offramp" onrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/onramp" seq1_6 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/sequences" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/router" api "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore" "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" ) type ConfigImportAdapter struct { - FeeQuoter map[uint64]common.Address - OnRamp map[uint64]common.Address - OffRamp map[uint64]common.Address - Router map[uint64]common.Address - TokenAdminReg map[uint64]common.Address + FeeQuoter common.Address + OnRamp common.Address + OffRamp common.Address + Router common.Address + TokenAdminReg common.Address } -func (ci *ConfigImportAdapter) InitializeAdapter(e cldf.Environment, selectors []uint64) error { - ci.FeeQuoter = make(map[uint64]common.Address) - ci.Router = make(map[uint64]common.Address) - ci.TokenAdminReg = make(map[uint64]common.Address) - ci.OnRamp = make(map[uint64]common.Address) - ci.OffRamp = make(map[uint64]common.Address) - for _, chainSelector := range selectors { - fqRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ - Type: datastore.ContractType(fqops.ContractType), - Version: fqops.Version, - ChainSelector: chainSelector, - }, chainSelector, evm_datastore_utils.ToEVMAddress) - if err != nil { - return fmt.Errorf("failed to find fee quoter contract ref for chain %d: %w", chainSelector, err) - } - ci.FeeQuoter[chainSelector] = fqRef - routerRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ - Type: datastore.ContractType("Router"), - Version: semver.MustParse("1.2.0"), - ChainSelector: chainSelector, - }, chainSelector, evm_datastore_utils.ToEVMAddress) - if err != nil { - return fmt.Errorf("failed to find router contract ref for chain %d: %w", chainSelector, err) - } - ci.Router[chainSelector] = routerRef - tokenAdminRegRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ - Type: datastore.ContractType(tokenadminops.ContractType), - Version: tokenadminops.Version, - ChainSelector: chainSelector, - }, chainSelector, evm_datastore_utils.ToEVMAddress) - if err != nil { - return fmt.Errorf("failed to find token admin registry contract ref for chain %d: %w", chainSelector, err) - } - ci.TokenAdminReg[chainSelector] = tokenAdminRegRef - onRampRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ - Type: datastore.ContractType(onrampops.ContractType), - Version: semver.MustParse("1.6.0"), - }, chainSelector, evm_datastore_utils.ToEVMAddress) - if err != nil { - return fmt.Errorf("failed to find onramp contract ref for chain %d: %w", chainSelector, err) - } - ci.OnRamp[chainSelector] = onRampRef - offRampRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ - Type: datastore.ContractType(offrampops.ContractType), - Version: semver.MustParse("1.6.0"), - }, chainSelector, evm_datastore_utils.ToEVMAddress) - if err != nil { - return fmt.Errorf("failed to find offramp contract ref for chain %d: %w", chainSelector, err) - } - ci.OffRamp[chainSelector] = offRampRef +func (ci *ConfigImportAdapter) InitializeAdapter(e cldf.Environment, chainSelector uint64) error { + fqRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ + Type: datastore.ContractType(fqops.ContractType), + Version: fqops.Version, + ChainSelector: chainSelector, + }, chainSelector, evm_datastore_utils.ToEVMAddress) + if err != nil { + return fmt.Errorf("failed to find fee quoter contract ref for chain %d: %w", chainSelector, err) } + ci.FeeQuoter = fqRef + routerRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ + Type: datastore.ContractType("Router"), + Version: semver.MustParse("1.2.0"), + ChainSelector: chainSelector, + }, chainSelector, evm_datastore_utils.ToEVMAddress) + if err != nil { + return fmt.Errorf("failed to find router contract ref for chain %d: %w", chainSelector, err) + } + ci.Router = routerRef + tokenAdminRegRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ + Type: datastore.ContractType(tokenadminops.ContractType), + Version: tokenadminops.Version, + ChainSelector: chainSelector, + }, chainSelector, evm_datastore_utils.ToEVMAddress) + if err != nil { + return fmt.Errorf("failed to find token admin registry contract ref for chain %d: %w", chainSelector, err) + } + ci.TokenAdminReg = tokenAdminRegRef + onRampRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ + Type: datastore.ContractType(onrampops.ContractType), + Version: semver.MustParse("1.6.0"), + }, chainSelector, evm_datastore_utils.ToEVMAddress) + if err != nil { + return fmt.Errorf("failed to find onramp contract ref for chain %d: %w", chainSelector, err) + } + ci.OnRamp = onRampRef + offRampRef, err := datastore_utils.FindAndFormatRef(e.DataStore, datastore.AddressRef{ + Type: datastore.ContractType(offrampops.ContractType), + Version: semver.MustParse("1.6.0"), + }, chainSelector, evm_datastore_utils.ToEVMAddress) + if err != nil { + return fmt.Errorf("failed to find offramp contract ref for chain %d: %w", chainSelector, err) + } + ci.OffRamp = offRampRef return nil } @@ -91,12 +85,8 @@ func (ci *ConfigImportAdapter) SupportedTokensPerRemoteChain(e cldf.Environment, if !ok { return nil, fmt.Errorf("chain with selector %d not found in environment", chainsel) } - tokenAdminRegAddr, ok := ci.TokenAdminReg[chainsel] - if !ok { - return nil, fmt.Errorf("token admin registry address not found for chain %d", chainsel) - } // get all supported tokens from token admin registry - return adapters1_5.GetSupportedTokensPerRemoteChain(tokenAdminRegAddr, chain) + return adapters1_5.GetSupportedTokensPerRemoteChain(ci.TokenAdminReg, chain) } func (ci *ConfigImportAdapter) ConnectedChains(e cldf.Environment, chainsel uint64) ([]uint64, error) { @@ -104,12 +94,12 @@ func (ci *ConfigImportAdapter) ConnectedChains(e cldf.Environment, chainsel uint if !ok { return nil, fmt.Errorf("chain with selector %d not found in environment", chainsel) } - routerAddr, ok := ci.Router[chainsel] - if !ok { - return nil, fmt.Errorf("router address not found for chain %d", chainsel) + routerAddr := ci.Router + if routerAddr == (common.Address{}) { + return nil, fmt.Errorf("router address not initialized for chain %d", chainsel) } // get all offRamps from router to find connected chains - routerC, err := router.NewRouter(routerAddr, chain.Client) + routerC, err := router.NewRouter(ci.Router, chain.Client) if err != nil { return nil, fmt.Errorf("failed to instantiate router contract at %s on chain %d: %w", routerAddr.String(), chain.Selector, err) } @@ -121,10 +111,10 @@ func (ci *ConfigImportAdapter) ConnectedChains(e cldf.Environment, chainsel uint } connectedChains := make([]uint64, 0) for _, offRamp := range offRamps { - if offRamp.OffRamp == (common.Address{}) { - continue // skip uninitialized off-ramps + // if the offramp's address matches our offramp, then we are connected to the source chain via 1.6 + if offRamp.OffRamp == ci.OffRamp { + connectedChains = append(connectedChains, offRamp.SourceChainSelector) } - connectedChains = append(connectedChains, offRamp.SourceChainSelector) } return connectedChains, nil } @@ -142,9 +132,9 @@ func (ci *ConfigImportAdapter) SequenceImportConfig() *cldf_ops.Sequence[api.Imp chainSelector := in.ChainSelector b.Logger.Infof("Importing configuration for chain %d (%s)", chainSelector, evmChain.Name()) // read FQ config from onchain - fqAddress, ok := ci.FeeQuoter[chainSelector] - if !ok { - return sequences.OnChainOutput{}, fmt.Errorf("fee quoter address not found for chain %d", chainSelector) + fqAddress := ci.FeeQuoter + if fqAddress == (common.Address{}) { + return sequences.OnChainOutput{}, fmt.Errorf("fee quoter address not initialized for chain %d", chainSelector) } var result sequences.OnChainOutput // fetch fee quoter config @@ -160,9 +150,9 @@ func (ci *ConfigImportAdapter) SequenceImportConfig() *cldf_ops.Sequence[api.Imp return sequences.OnChainOutput{}, fmt.Errorf("failed to import fee quoter config on chain %d: %w", chainSelector, err) } // fetch onramp config - onRampAddress, ok := ci.OnRamp[chainSelector] - if !ok { - return sequences.OnChainOutput{}, fmt.Errorf("onramp address not found for chain %d", chainSelector) + onRampAddress := ci.OnRamp + if onRampAddress == (common.Address{}) { + return sequences.OnChainOutput{}, fmt.Errorf("onramp address not initialized for chain %d", chainSelector) } result, err = sequences.RunAndMergeSequence(b, chains, seq1_6.OnRampImportConfigSequence, @@ -175,9 +165,9 @@ func (ci *ConfigImportAdapter) SequenceImportConfig() *cldf_ops.Sequence[api.Imp return sequences.OnChainOutput{}, fmt.Errorf("failed to import onramp config on chain %d: %w", chainSelector, err) } // fetch offramp config - offRampAddress, ok := ci.OffRamp[chainSelector] - if !ok { - return sequences.OnChainOutput{}, fmt.Errorf("offramp address not found for chain %d", chainSelector) + offRampAddress := ci.OffRamp + if offRampAddress == (common.Address{}) { + return sequences.OnChainOutput{}, fmt.Errorf("offramp address not initialized for chain %d", chainSelector) } result, err = sequences.RunAndMergeSequence(b, chains, seq1_6.OffRampImportConfigSequence, diff --git a/chains/evm/deployment/v1_6_0/adapters/rampupdatewithfq.go b/chains/evm/deployment/v1_6_0/adapters/rampupdatewithfq.go new file mode 100644 index 0000000000..26a765fbd1 --- /dev/null +++ b/chains/evm/deployment/v1_6_0/adapters/rampupdatewithfq.go @@ -0,0 +1,201 @@ +package adapters + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_6_0/offramp" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + mcms_types "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract" + routerops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router" + offrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/offramp" + onrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/onramp" + "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" + datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" +) + +type RampUpdateWithFQ struct{} + +func (ru RampUpdateWithFQ) ResolveRampsInput(e cldf.Environment, input deploy.UpdateRampsInput) (deploy.UpdateRampsInput, error) { + // fetch address of Ramps + onRampAddr := datastore_utils.GetAddressRef( + e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(input.ChainSelector), + datastore.AddressRefByType(datastore.ContractType(onrampops.ContractType)), + datastore.AddressRefByVersion(onrampops.Version), + ), + input.ChainSelector, + onrampops.ContractType, + onrampops.Version, + "", + ) + if datastore_utils.IsAddressRefEmpty(onRampAddr) { + return input, fmt.Errorf("onramp address not found for chain selector %d", input.ChainSelector) + } + input.OnRampAddressRef = onRampAddr + + offRampAddr := datastore_utils.GetAddressRef( + e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(input.ChainSelector), + datastore.AddressRefByType(datastore.ContractType(offrampops.ContractType)), + datastore.AddressRefByVersion(offrampops.Version), + ), + input.ChainSelector, + offrampops.ContractType, + offrampops.Version, + "", + ) + if datastore_utils.IsAddressRefEmpty(offRampAddr) { + return input, fmt.Errorf("offramp address not found for chain selector %d", input.ChainSelector) + } + input.OffRampAddressRef = offRampAddr + routerAddr := datastore_utils.GetAddressRef( + e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(input.ChainSelector), + datastore.AddressRefByType(datastore.ContractType(routerops.ContractType)), + datastore.AddressRefByVersion(routerops.Version), + ), + input.ChainSelector, + routerops.ContractType, + routerops.Version, + "", + ) + if datastore_utils.IsAddressRefEmpty(routerAddr) { + return input, fmt.Errorf("router address not found for chain selector %d", input.ChainSelector) + } + for srcChain, srcChainConfig := range input.SourceChains { + srcOnRampAddr := datastore_utils.GetAddressRef( + e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(srcChain), + datastore.AddressRefByType(datastore.ContractType(onrampops.ContractType)), + datastore.AddressRefByVersion(onrampops.Version), + ), + srcChain, + onrampops.ContractType, + onrampops.Version, + "", + ) + if datastore_utils.IsAddressRefEmpty(srcOnRampAddr) { + return input, fmt.Errorf("onramp address not found for source chain selector %d", srcChain) + } + srcChainConfig.OnRamp = srcOnRampAddr + srcChainConfig.Router = routerAddr + input.SourceChains[srcChain] = srcChainConfig + } + return input, nil +} + +// SequenceUpdateRampsWithFeeQuoter updates OnRamp and OffRamp contracts to use the new FeeQuoter contract +// It also updates OffRamp source chain configs if provided in the input +func (ru RampUpdateWithFQ) SequenceUpdateRampsWithFeeQuoter() *cldf_ops.Sequence[deploy.UpdateRampsInput, sequences.OnChainOutput, cldf_chain.BlockChains] { + return cldf_ops.NewSequence( + "ramp-update-with-fq:sequence-update-ramps-with-fee-quoter", + semver.MustParse("1.6.0"), + "Updates Ramps contracts to use the new FeeQuoter contract", + func(b cldf_ops.Bundle, chains cldf_chain.BlockChains, input deploy.UpdateRampsInput) (output sequences.OnChainOutput, err error) { + var writes []contract.WriteOutput + chain, ok := chains.EVMChains()[input.ChainSelector] + if !ok { + return sequences.OnChainOutput{}, fmt.Errorf("chain with selector %d not found in environment", input.ChainSelector) + } + onDCfgReport, err := cldf_ops.ExecuteOperation(b, onrampops.GetDynamicConfig, chain, contract.FunctionInput[any]{ + ChainSelector: input.ChainSelector, + Address: common.HexToAddress(input.OnRampAddressRef.Address), + Args: nil, + }) + if err != nil { + return sequences.OnChainOutput{}, err + } + existingDynamicConfig := onDCfgReport.Output + if existingDynamicConfig.FeeQuoter != common.HexToAddress(input.FeeQuoterAddress.Address) { + // Update OnRamp's FeeQuoter address + existingDynamicConfig.FeeQuoter = common.HexToAddress(input.FeeQuoterAddress.Address) + onRampReport, err := cldf_ops.ExecuteOperation(b, onrampops.OnRampSetDynamicConfig, chain, contract.FunctionInput[onrampops.DynamicConfig]{ + ChainSelector: input.ChainSelector, + Address: common.HexToAddress(input.OnRampAddressRef.Address), + Args: existingDynamicConfig, + }) + if err != nil { + return sequences.OnChainOutput{}, err + } + writes = append(writes, onRampReport.Output) + } + // Similarly, update OffRamp's FeeQuoter address + offDCfgReport, err := cldf_ops.ExecuteOperation(b, offrampops.GetDynamicConfig, chain, contract.FunctionInput[any]{ + ChainSelector: input.ChainSelector, + Address: common.HexToAddress(input.OffRampAddressRef.Address), + Args: nil, + }) + if err != nil { + return sequences.OnChainOutput{}, err + } + existingOffDynamicConfig := offDCfgReport.Output + if existingOffDynamicConfig.FeeQuoter != common.HexToAddress(input.FeeQuoterAddress.Address) { + existingOffDynamicConfig.FeeQuoter = common.HexToAddress(input.FeeQuoterAddress.Address) + offRampReport, err := cldf_ops.ExecuteOperation(b, offrampops.OffRampSetDynamicConfig, chain, contract.FunctionInput[offrampops.DynamicConfig]{ + ChainSelector: input.ChainSelector, + Address: common.HexToAddress(input.OffRampAddressRef.Address), + Args: existingOffDynamicConfig, + }) + if err != nil { + return sequences.OnChainOutput{}, err + } + writes = append(writes, offRampReport.Output) + } + if len(input.SourceChains) > 0 { + var sourceChainConfigs []offramp.OffRampSourceChainConfigArgs + for srcChainSelector, srcChainConfig := range input.SourceChains { + // Check if the source chain config already exists and is up to date + existingSrcChainsReport, err := cldf_ops.ExecuteOperation(b, offrampops.GetSourceChainConfig, chain, contract.FunctionInput[uint64]{ + Address: common.HexToAddress(input.OffRampAddressRef.Address), + ChainSelector: input.ChainSelector, + Args: srcChainSelector, + }) + if err != nil { + return sequences.OnChainOutput{}, err + } + existingSrcChainConfig := existingSrcChainsReport.Output + if existingSrcChainConfig.IsEnabled && + existingSrcChainConfig.Router == common.HexToAddress(srcChainConfig.Router.Address) { + continue + } + sourceChainConfigs = append(sourceChainConfigs, offramp.OffRampSourceChainConfigArgs{ + SourceChainSelector: srcChainSelector, + Router: common.HexToAddress(srcChainConfig.Router.Address), + OnRamp: common.Hex2Bytes(srcChainConfig.OnRamp.Address), + IsEnabled: true, + IsRMNVerificationDisabled: true, + }) + } + if len(sourceChainConfigs) > 0 { + offRampSrcReport, err := cldf_ops.ExecuteOperation( + b, offrampops.OffRampApplySourceChainConfigUpdates, + chain, contract.FunctionInput[[]offramp.OffRampSourceChainConfigArgs]{ + Address: common.HexToAddress(input.OffRampAddressRef.Address), + ChainSelector: input.ChainSelector, + Args: sourceChainConfigs, + }, + ) + if err != nil { + return sequences.OnChainOutput{}, err + } + writes = append(writes, offRampSrcReport.Output) + } + } + batch, err := contract.NewBatchOperationFromWrites(writes) + if err != nil { + return sequences.OnChainOutput{}, err + } + return sequences.OnChainOutput{ + BatchOps: []mcms_types.BatchOperation{batch}, + }, nil + }, + ) +} diff --git a/chains/evm/deployment/v1_6_0/operations/fee_quoter/fee_quoter.go b/chains/evm/deployment/v1_6_0/operations/fee_quoter/fee_quoter.go index c6ace2535f..2d017cb65f 100644 --- a/chains/evm/deployment/v1_6_0/operations/fee_quoter/fee_quoter.go +++ b/chains/evm/deployment/v1_6_0/operations/fee_quoter/fee_quoter.go @@ -159,3 +159,25 @@ var GetTokenTransferFeeConfig = contract.NewRead(contract.ReadParams[GetTokenTra return feeQuoter.GetTokenTransferFeeConfig(opts, in.DestChainSelector, in.Token) }, }) + +var GetStaticConfig = contract.NewRead(contract.ReadParams[any, fee_quoter.FeeQuoterStaticConfig, *fee_quoter.FeeQuoter]{ + Name: "fee-quoter:get-static-config", + Version: Version, + Description: "Gets the static config from the FeeQuoter 1.6.0 contract", + ContractType: ContractType, + NewContract: fee_quoter.NewFeeQuoter, + CallContract: func(feeQuoter *fee_quoter.FeeQuoter, opts *bind.CallOpts, args any) (fee_quoter.FeeQuoterStaticConfig, error) { + return feeQuoter.GetStaticConfig(opts) + }, +}) + +var GetAllAuthorizedCallers = contract.NewRead(contract.ReadParams[any, []common.Address, *fee_quoter.FeeQuoter]{ + Name: "fee-quoter:get-all-authorized-callers", + Version: Version, + Description: "Gets all authorized callers (price updaters) from the FeeQuoter 1.6.0 contract", + ContractType: ContractType, + NewContract: fee_quoter.NewFeeQuoter, + CallContract: func(feeQuoter *fee_quoter.FeeQuoter, opts *bind.CallOpts, args any) ([]common.Address, error) { + return feeQuoter.GetAllAuthorizedCallers(opts) + }, +}) diff --git a/chains/evm/deployment/v1_6_0/operations/offramp/offramp.go b/chains/evm/deployment/v1_6_0/operations/offramp/offramp.go index 29e21066e9..b191ce0670 100644 --- a/chains/evm/deployment/v1_6_0/operations/offramp/offramp.go +++ b/chains/evm/deployment/v1_6_0/operations/offramp/offramp.go @@ -73,6 +73,20 @@ var OffRampSetOcr3 = contract.NewWrite(contract.WriteParams[[]offramp.MultiOCR3B }, }) +var OffRampSetDynamicConfig = contract.NewWrite(contract.WriteParams[DynamicConfig, *offramp.OffRamp]{ + Name: "offramp:set-dynamic-config", + Version: Version, + Description: "Sets the dynamic config on the OffRamp 1.6.0 contract", + ContractType: ContractType, + ContractABI: offramp.OffRampABI, + NewContract: offramp.NewOffRamp, + IsAllowedCaller: contract.OnlyOwner[*offramp.OffRamp, DynamicConfig], + Validate: func(DynamicConfig) error { return nil }, + CallContract: func(offRamp *offramp.OffRamp, opts *bind.TransactOpts, args DynamicConfig) (*types.Transaction, error) { + return offRamp.SetDynamicConfig(opts, args) + }, +}) + var GetStaticConfig = contract.NewRead(contract.ReadParams[any, offramp.OffRampStaticConfig, *offramp.OffRamp]{ Name: "offramp:get-static-config", Version: Version, diff --git a/chains/evm/deployment/v1_6_0/operations/onramp/onramp.go b/chains/evm/deployment/v1_6_0/operations/onramp/onramp.go index 4ab22c8b01..4b672697c2 100644 --- a/chains/evm/deployment/v1_6_0/operations/onramp/onramp.go +++ b/chains/evm/deployment/v1_6_0/operations/onramp/onramp.go @@ -52,6 +52,20 @@ var OnRampApplyDestChainConfigUpdates = contract.NewWrite(contract.WriteParams[[ }, }) +var OnRampSetDynamicConfig = contract.NewWrite(contract.WriteParams[DynamicConfig, *onramp.OnRamp]{ + Name: "onramp:set-dynamic-config", + Version: Version, + Description: "Sets the dynamic config on the OnRamp 1.6.0 contract", + ContractType: ContractType, + ContractABI: onramp.OnRampABI, + NewContract: onramp.NewOnRamp, + IsAllowedCaller: contract.OnlyOwner[*onramp.OnRamp, DynamicConfig], + Validate: func(DynamicConfig) error { return nil }, + CallContract: func(onRamp *onramp.OnRamp, opts *bind.TransactOpts, args DynamicConfig) (*types.Transaction, error) { + return onRamp.SetDynamicConfig(opts, args) + }, +}) + var GetDestChainConfig = contract.NewRead(contract.ReadParams[uint64, onramp.GetDestChainConfig, *onramp.OnRamp]{ Name: "onramp:get-dest-chain-config", Version: Version, diff --git a/chains/evm/deployment/v1_6_0/sequences/fee_quoter.go b/chains/evm/deployment/v1_6_0/sequences/fee_quoter.go index 4f5227b389..f5e7739fa8 100644 --- a/chains/evm/deployment/v1_6_0/sequences/fee_quoter.go +++ b/chains/evm/deployment/v1_6_0/sequences/fee_quoter.go @@ -9,13 +9,13 @@ import ( "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_6_3/fee_quoter" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract" - fqops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/fee_quoter" - "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" - cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" "github.com/smartcontractkit/chainlink-deployments-framework/operations" mcms_types "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract" + fqops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/fee_quoter" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" ) type FeeQuoterApplyDestChainConfigUpdatesSequenceInput struct { @@ -44,6 +44,12 @@ type FeeQuoterImportConfigSequenceInput struct { } type FeeQuoterImportConfigSequenceOutput struct { + RemoteChainCfgs map[uint64]FeeQuoterImportConfigSequenceOutputPerRemoteChain + PriceUpdaters []common.Address + StaticCfg fee_quoter.FeeQuoterStaticConfig +} + +type FeeQuoterImportConfigSequenceOutputPerRemoteChain struct { DestChainCfg fee_quoter.FeeQuoterDestChainConfig TokenTransferFeeCfgs map[common.Address]fee_quoter.FeeQuoterTokenTransferFeeConfig } @@ -149,7 +155,7 @@ var ( fqAddress := in.Address chainSelector := in.ChainSelector b.Logger.Infof("Importing configuration for FeeQuoter %s on chain %d (%s)", fqAddress.Hex(), chainSelector, evmChain.Name()) - fqOutput := make(map[uint64]FeeQuoterImportConfigSequenceOutput) + fqOutput := make(map[uint64]FeeQuoterImportConfigSequenceOutputPerRemoteChain) destChainConfigs := make(map[uint64]fee_quoter.FeeQuoterDestChainConfig) for _, remoteChain := range in.RemoteChains { opsOutput, err := operations.ExecuteOperation(b, fqops.GetDestChainConfig, evmChain, contract.FunctionInput[uint64]{ @@ -162,12 +168,19 @@ var ( "remote chain %d from feequoter %s on chain %d: %w", remoteChain, fqAddress.Hex(), chainSelector, err) } + if !opsOutput.Output.IsEnabled { + continue // skip disabled dest chain configs + } + destChainConfigs[remoteChain] = opsOutput.Output } tokenTransferFeeCfgsPerChain := make(map[uint64]map[common.Address]fee_quoter.FeeQuoterTokenTransferFeeConfig) for remoteChain, tokens := range in.TokensPerRemoteChain { + if _, ok := destChainConfigs[remoteChain]; !ok { + continue // skip token transfer fee config fetching if dest chain config is not enabled + } tokenTransferFeeCfgs := make(map[common.Address]fee_quoter.FeeQuoterTokenTransferFeeConfig) for _, token := range tokens { opsOutput, err := operations.ExecuteOperation(b, fqops.GetTokenTransferFeeConfig, evmChain, @@ -191,16 +204,38 @@ var ( tokenTransferFeeCfgsPerChain[remoteChain] = tokenTransferFeeCfgs } for remoteChain, destCfg := range destChainConfigs { - fqOutput[remoteChain] = FeeQuoterImportConfigSequenceOutput{ + fqOutput[remoteChain] = FeeQuoterImportConfigSequenceOutputPerRemoteChain{ DestChainCfg: destCfg, TokenTransferFeeCfgs: tokenTransferFeeCfgsPerChain[remoteChain], } } + staticCfgOutput, err := operations.ExecuteOperation(b, fqops.GetStaticConfig, evmChain, contract.FunctionInput[any]{ + Address: fqAddress, + ChainSelector: chainSelector, + Args: nil, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to get static config from feequoter %s on chain %d: %w", + fqAddress.Hex(), chainSelector, err) + } + priceUpdaters, err := operations.ExecuteOperation(b, fqops.GetAllAuthorizedCallers, evmChain, contract.FunctionInput[any]{ + Address: fqAddress, + ChainSelector: chainSelector, + Args: nil, + }) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to get all authorized callers from feequoter %s on chain %d: %w", + fqAddress.Hex(), chainSelector, err) + } contractMetadata = []datastore.ContractMetadata{ { Address: fqAddress.Hex(), ChainSelector: chainSelector, - Metadata: fqOutput, + Metadata: FeeQuoterImportConfigSequenceOutput{ + RemoteChainCfgs: fqOutput, + StaticCfg: staticCfgOutput.Output, + PriceUpdaters: priceUpdaters.Output, + }, }, } return sequences.OnChainOutput{ diff --git a/deployment/deploy/feequoterupdater.go b/deployment/deploy/feequoterupdater.go new file mode 100644 index 0000000000..e727d26dd0 --- /dev/null +++ b/deployment/deploy/feequoterupdater.go @@ -0,0 +1,293 @@ +package deploy + +import ( + "fmt" + "sync" + + "github.com/Masterminds/semver/v3" + chain_selectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + mcms_types "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/mcms" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" +) + +var ( + singletonFQAndRampUpdaterRegistry *FQAndRampUpdaterRegistry + fqupdaterOnce sync.Once +) + +type UpdateFeeQuoterInput struct { + Chains map[uint64]UpdateFeeQuoterInputPerChain + MCMS mcms.Input +} + +type UpdateFeeQuoterInputPerChain struct { + ImportFeeQuoterConfigFromVersions []*semver.Version + FeeQuoterVersion *semver.Version + RampsVersion *semver.Version + SourceChainAddsToOffRamp []uint64 +} + +type FeeQuoterUpdateInput struct { + ChainSelector uint64 + ExistingAddresses []datastore.AddressRef + ContractMeta []datastore.ContractMetadata +} + +type SourceChainConfig struct { + Router datastore.AddressRef + OnRamp datastore.AddressRef +} + +type UpdateRampsInput struct { + ChainSelector uint64 + FeeQuoterAddress datastore.AddressRef + OnRampAddressRef datastore.AddressRef + OffRampAddressRef datastore.AddressRef + SourceChains map[uint64]SourceChainConfig +} + +// FeeQuoterUpdater provides methods to update FeeQuoter contract on a chain. +type FeeQuoterUpdater[FeeQUpdateArgs any] interface { + SequenceFeeQuoterInputCreation() *cldf_ops.Sequence[FeeQuoterUpdateInput, FeeQUpdateArgs, chain.BlockChains] + SequenceDeployOrUpdateFeeQuoter() *cldf_ops.Sequence[FeeQUpdateArgs, sequences.OnChainOutput, chain.BlockChains] +} + +type RampUpdater interface { + ResolveRampsInput(e cldf.Environment, input UpdateRampsInput) (UpdateRampsInput, error) + SequenceUpdateRampsWithFeeQuoter() *cldf_ops.Sequence[UpdateRampsInput, sequences.OnChainOutput, chain.BlockChains] +} + +type FQAndRampUpdaterRegistry struct { + FeeQuoterUpdater map[string]FeeQuoterUpdater[any] + RampUpdater map[string]RampUpdater + ConfigImporter map[string]ConfigImporter + mu sync.Mutex +} + +func (r *FQAndRampUpdaterRegistry) RegisterFeeQuoterUpdater(family string, version *semver.Version, updater FeeQuoterUpdater[any]) { + r.mu.Lock() + defer r.mu.Unlock() + id := utils.NewRegistererID(family, version) + if _, exists := r.FeeQuoterUpdater[id]; !exists { + r.FeeQuoterUpdater[id] = updater + } +} + +func (r *FQAndRampUpdaterRegistry) RegisterRampUpdater(family string, version *semver.Version, updater RampUpdater) { + r.mu.Lock() + defer r.mu.Unlock() + id := utils.NewRegistererID(family, version) + if _, exists := r.RampUpdater[id]; !exists { + r.RampUpdater[id] = updater + } +} + +func (r *FQAndRampUpdaterRegistry) RegisterConfigImporter(family string, version *semver.Version, importer ConfigImporter) { + r.mu.Lock() + defer r.mu.Unlock() + id := utils.NewRegistererID(family, version) + if _, exists := r.ConfigImporter[id]; !exists { + r.ConfigImporter[id] = importer + } +} + +func (r *FQAndRampUpdaterRegistry) GetConfigImporter(chainsel uint64, version *semver.Version) (ConfigImporter, bool) { + // Get the chain family from the chain selector + family, err := chain_selectors.GetSelectorFamily(chainsel) + if err != nil { + return nil, false + } + r.mu.Lock() + defer r.mu.Unlock() + id := utils.NewRegistererID(family, version) + importer, ok := r.ConfigImporter[id] + return importer, ok +} + +func (r *FQAndRampUpdaterRegistry) GetFeeQuoterUpdater(chainsel uint64, version *semver.Version) (FeeQuoterUpdater[any], bool) { + // Get the chain family from the chain selector + family, err := chain_selectors.GetSelectorFamily(chainsel) + if err != nil { + return nil, false + } + r.mu.Lock() + defer r.mu.Unlock() + id := utils.NewRegistererID(family, version) + updater, ok := r.FeeQuoterUpdater[id] + return updater, ok +} + +func (r *FQAndRampUpdaterRegistry) GetRampUpdater(chainsel uint64, version *semver.Version) (RampUpdater, bool) { + // Get the chain family from the chain selector + family, err := chain_selectors.GetSelectorFamily(chainsel) + if err != nil { + return nil, false + } + r.mu.Lock() + defer r.mu.Unlock() + id := utils.NewRegistererID(family, version) + updater, ok := r.RampUpdater[id] + return updater, ok +} + +func NewFQUpdaterRegistry() *FQAndRampUpdaterRegistry { + return &FQAndRampUpdaterRegistry{ + FeeQuoterUpdater: make(map[string]FeeQuoterUpdater[any]), + RampUpdater: make(map[string]RampUpdater), + ConfigImporter: make(map[string]ConfigImporter), + } +} + +func GetFQAndRampUpdaterRegistry() *FQAndRampUpdaterRegistry { + fqupdaterOnce.Do(func() { + singletonFQAndRampUpdaterRegistry = NewFQUpdaterRegistry() + }) + return singletonFQAndRampUpdaterRegistry +} + +// UpdateFeeQuoterChangeset creates a changeset that updates FeeQuoter contracts on specified chains. +// It first optionally populates configuration values, then creates FeeQuoterUpdateInput, +// deploys or updates the FeeQuoter contract, and finally updates the Ramps contracts to use the new FeeQuoter address. +// If needed, it also updates OffRamp source chain configs ( specifically used when during updating feequoter only specific source chain needs to be added to offramp). +func UpdateFeeQuoterChangeset(fquRegistry *FQAndRampUpdaterRegistry, mcmsRegistry *changesets.MCMSReaderRegistry) cldf.ChangeSetV2[UpdateFeeQuoterInput] { + return cldf.CreateChangeSet(updateFeeQuoterApply(fquRegistry, mcmsRegistry), updateFeeQuoterVerify(fquRegistry, mcmsRegistry)) +} + +func updateFeeQuoterVerify(fquRegistry *FQAndRampUpdaterRegistry, mcmsRegistry *changesets.MCMSReaderRegistry) func(cldf.Environment, UpdateFeeQuoterInput) error { + return func(e cldf.Environment, input UpdateFeeQuoterInput) error { + for chainSel, perChainInput := range input.Chains { + if !e.BlockChains.Exists(chainSel) { + return fmt.Errorf("chain with selector %d not found in environment", chainSel) + } + if perChainInput.FeeQuoterVersion == nil { + return fmt.Errorf("fee quoter version is required for chain selector %d", chainSel) + } + if perChainInput.RampsVersion == nil { + return fmt.Errorf("ramps version is required for chain selector %d", chainSel) + } + } + return nil + } +} + +func updateFeeQuoterApply(fquRegistry *FQAndRampUpdaterRegistry, mcmsRegistry *changesets.MCMSReaderRegistry) func(cldf.Environment, UpdateFeeQuoterInput) (cldf.ChangesetOutput, error) { + return func(e cldf.Environment, input UpdateFeeQuoterInput) (cldf.ChangesetOutput, error) { + batchOps := make([]mcms_types.BatchOperation, 0) + reports := make([]cldf_ops.Report[any, any], 0) + addressRefs := make([]datastore.AddressRef, 0) + contractMetadata := make([]datastore.ContractMetadata, 0) + for chainSel, perChainInput := range input.Chains { + fquUpdater, ok := fquRegistry.GetFeeQuoterUpdater(chainSel, perChainInput.FeeQuoterVersion) + if !ok { + return cldf.ChangesetOutput{}, utils.ErrNoAdapterRegistered("FeeQuoterUpdater", perChainInput.FeeQuoterVersion) + } + rampUpdater, ok := fquRegistry.GetRampUpdater(chainSel, perChainInput.RampsVersion) + if !ok { + return cldf.ChangesetOutput{}, utils.ErrNoAdapterRegistered("RampUpdater", perChainInput.RampsVersion) + } + contractMeta := e.DataStore.ContractMetadata().Filter(datastore.ContractMetadataByChainSelector(chainSel)) + for _, version := range perChainInput.ImportFeeQuoterConfigFromVersions { + configImporter, ok := fquRegistry.GetConfigImporter(chainSel, version) + if !ok { + return cldf.ChangesetOutput{}, utils.ErrNoAdapterRegistered("ConfigImporter", perChainInput.FeeQuoterVersion) + } + err := configImporter.InitializeAdapter(e, chainSel) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to initialize config importer for chain %d: %w", chainSel, err) + } + supportedTokensPerRemoteChain, err := configImporter.SupportedTokensPerRemoteChain(e, chainSel) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to get supported tokens per remote chain for chain %d: %w", chainSel, err) + } + connectedChains, err := configImporter.ConnectedChains(e, chainSel) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to get connected chains for chain %d: %w", chainSel, err) + } + populateConfigReport, err := cldf_ops.ExecuteSequence(e.OperationsBundle, configImporter.SequenceImportConfig(), e.BlockChains, ImportConfigPerChainInput{ + ChainSelector: chainSel, + RemoteChains: connectedChains, + TokensPerRemoteChain: supportedTokensPerRemoteChain, + }) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to populate config for FeeQuoter on chain %d: %w", chainSel, err) + } + if len(populateConfigReport.Output.Metadata.Contracts) == 0 { + return cldf.ChangesetOutput{}, fmt.Errorf("no contract metadata returned from populate config for FeeQuoter on chain %d", chainSel) + } + contractMeta = append(contractMeta, populateConfigReport.Output.Metadata.Contracts...) + contractMetadata = append(contractMetadata, populateConfigReport.Output.Metadata.Contracts...) + } + // Create FeeQuoterUpdateInput + reportFQInputCreation, err := cldf_ops.ExecuteSequence(e.OperationsBundle, fquUpdater.SequenceFeeQuoterInputCreation(), e.BlockChains, FeeQuoterUpdateInput{ + ChainSelector: chainSel, + ExistingAddresses: e.DataStore.Addresses().Filter(datastore.AddressRefByChainSelector(chainSel)), + ContractMeta: contractMeta, + }) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to create FeeQuoterUpdateInput for chain %d: %w", chainSel, err) + } + // Deploy or update FeeQuoter + reportFQUpdate, err := cldf_ops.ExecuteSequence(e.OperationsBundle, fquUpdater.SequenceDeployOrUpdateFeeQuoter(), e.BlockChains, reportFQInputCreation.Output) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to deploy or update FeeQuoter for chain %d: %w", chainSel, err) + } + batchOps = append(batchOps, reportFQUpdate.Output.BatchOps...) + addressRefs = append(addressRefs, reportFQUpdate.Output.Addresses...) + reports = append(reports, reportFQUpdate.ExecutionReports...) + if len(reportFQUpdate.Output.Addresses) == 0 { + return cldf.ChangesetOutput{}, fmt.Errorf("no FeeQuoter address returned for chain %d", chainSel) + } + // Update Ramps with new FeeQuoter address + // fetch the address refs + feeQuoterAddrRef := reportFQUpdate.Output.Addresses[len(reportFQUpdate.Output.Addresses)-1] + if perChainInput.RampsVersion != nil { + rampsInput := UpdateRampsInput{ + ChainSelector: chainSel, + FeeQuoterAddress: feeQuoterAddrRef, + SourceChains: make(map[uint64]SourceChainConfig), + } + for _, srcChainSel := range perChainInput.SourceChainAddsToOffRamp { + rampsInput.SourceChains[srcChainSel] = SourceChainConfig{} + } + // Resolve Ramps input + resolvedRampsInput, err := rampUpdater.ResolveRampsInput(e, rampsInput) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to resolve ramps input for chain %d: %w", chainSel, err) + } + // Execute Ramps update sequence + reportRampsUpdate, err := cldf_ops.ExecuteSequence(e.OperationsBundle, rampUpdater.SequenceUpdateRampsWithFeeQuoter(), e.BlockChains, resolvedRampsInput) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to update ramps with FeeQuoter for chain %d: %w", chainSel, err) + } + batchOps = append(batchOps, reportRampsUpdate.Output.BatchOps...) + addressRefs = append(addressRefs, reportRampsUpdate.Output.Addresses...) + reports = append(reports, reportRampsUpdate.ExecutionReports...) + } + } + // Prepare datastore with all address refs + ds := datastore.NewMemoryDataStore() + for _, addrRef := range addressRefs { + if err := ds.Addresses().Add(addrRef); err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to add %s %s with address %s on chain with selector %d to datastore: %w", addrRef.Type, addrRef.Version, addrRef.Address, addrRef.ChainSelector, err) + } + } + if err := sequences.WriteMetadataToDatastore(ds, sequences.Metadata{ + Contracts: contractMetadata, + }); err != nil { + return cldf.ChangesetOutput{Reports: reports}, fmt.Errorf("failed to write metadata to datastore: %w", err) + } + return changesets.NewOutputBuilder(e, mcmsRegistry). + WithReports(reports). + WithDataStore(ds). + WithBatchOps(batchOps). + Build(input.MCMS) + } +} diff --git a/deployment/deploy/product.go b/deployment/deploy/product.go index d42a9c84d8..d5f5b3b064 100644 --- a/deployment/deploy/product.go +++ b/deployment/deploy/product.go @@ -96,7 +96,7 @@ type TransferOwnershipAdapter interface { } type ConfigImporter interface { - InitializeAdapter(e cldf.Environment, selectors []uint64) error + InitializeAdapter(e cldf.Environment, selectors uint64) error ConnectedChains(e cldf.Environment, chainsel uint64) ([]uint64, error) SupportedTokensPerRemoteChain(e cldf.Environment, chainSelector uint64) (map[uint64][]common.Address, error) SequenceImportConfig() *cldf_ops.Sequence[ImportConfigPerChainInput, sequences.OnChainOutput, cldf_chain.BlockChains] diff --git a/deployment/utils/datastore/datastore.go b/deployment/utils/datastore/datastore.go index 71bec0599b..8d8c7279bb 100644 --- a/deployment/utils/datastore/datastore.go +++ b/deployment/utils/datastore/datastore.go @@ -1,6 +1,7 @@ package datastore import ( + "encoding/json" "fmt" "strings" @@ -145,3 +146,71 @@ func GetAddressRef( } return datastore.AddressRef{} } + +func FilterContractMetaByContractTypeAndVersion( + addressRefs []datastore.AddressRef, + contractMetadata []datastore.ContractMetadata, + contractType cldf.ContractType, + contractVersion *semver.Version, + qualifier string, + chainSelector uint64, +) ([]datastore.ContractMetadata, error) { + ds := datastore.NewMemoryDataStore() + for _, ref := range addressRefs { + if err := ds.Addresses().Add(ref); err != nil { + return nil, fmt.Errorf("failed to add address ref to datastore: %w", err) + } + } + filterFns := []datastore.FilterFunc[datastore.AddressRefKey, datastore.AddressRef]{ + datastore.AddressRefByChainSelector(chainSelector), + datastore.AddressRefByType(datastore.ContractType(contractType)), + datastore.AddressRefByVersion(contractVersion), + } + if qualifier != "" { + filterFns = append(filterFns, datastore.AddressRefByQualifier(qualifier)) + } + filteredAddressRefs := ds.Addresses().Filter(filterFns...) + + if len(filteredAddressRefs) == 0 { + return nil, fmt.Errorf("no address ref found for contract type %s and version %s on chain %d", + contractType, contractVersion.String(), chainSelector) + } + var filteredContractMetadata []datastore.ContractMetadata + for _, meta := range contractMetadata { + for _, ref := range filteredAddressRefs { + if meta.Address == ref.Address && meta.ChainSelector == ref.ChainSelector { + filteredContractMetadata = append(filteredContractMetadata, meta) + } + } + } + return filteredContractMetadata, nil +} + +// ConvertMetadataToType converts metadata to a typed struct +// Handles both typed structs and map[string]interface{} from JSON unmarshaling +// T is the target type that the metadata should be converted to +func ConvertMetadataToType[T any](metadata interface{}) (T, error) { + var zero T + + // If already the correct type, return it + if typed, ok := metadata.(T); ok { + return typed, nil + } + + // If it's a map (from JSON), convert it + if metaMap, ok := metadata.(map[string]interface{}); ok { + metadataBytes, err := json.Marshal(metaMap) + if err != nil { + return zero, fmt.Errorf("failed to marshal metadata: %w", err) + } + + var typed T + if err := json.Unmarshal(metadataBytes, &typed); err != nil { + return zero, fmt.Errorf("failed to unmarshal metadata: %w", err) + } + + return typed, nil + } + + return zero, fmt.Errorf("metadata is neither the expected type nor map[string]interface{}") +} diff --git a/deployment/utils/datastore/datastore_test.go b/deployment/utils/datastore/datastore_test.go index 55e1451a41..55fc7ae1c4 100644 --- a/deployment/utils/datastore/datastore_test.go +++ b/deployment/utils/datastore/datastore_test.go @@ -4,9 +4,10 @@ import ( "testing" "github.com/Masterminds/semver/v3" - datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/stretchr/testify/require" + + datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore" ) func TestFindAndFormatRef(t *testing.T) { diff --git a/go.md b/go.md index b6c9c496f3..efca223f86 100644 --- a/go.md +++ b/go.md @@ -102,7 +102,7 @@ flowchart LR chainlink-ccip/devenv --> chainlink-ccip/chains/solana/deployment chainlink-ccip/devenv --> chainlink-ton/devenv click chainlink-ccip/devenv href "https://github.com/smartcontractkit/chainlink-ccip" - chainlink-ccip/integration-tests --> chainlink-ccip/chains/evm/deployment + chainlink-ccip/integration-tests --> chainlink-ccip/ccv/chains/evm/deployment chainlink-ccip/integration-tests --> chainlink-ccip/chains/solana/deployment click chainlink-ccip/integration-tests href "https://github.com/smartcontractkit/chainlink-ccip" chainlink-common --> chain-selectors diff --git a/integration-tests/deployment/update_to_FeeQuoter_1_7_test.go b/integration-tests/deployment/update_to_FeeQuoter_1_7_test.go new file mode 100644 index 0000000000..c06a9ec093 --- /dev/null +++ b/integration-tests/deployment/update_to_FeeQuoter_1_7_test.go @@ -0,0 +1,178 @@ +package deployment + +import ( + "math/big" + "testing" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/common" + chain_selectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/stretchr/testify/require" + + _ "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/adapters" + fqops "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/fee_quoter" + "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/gobindings/generated/latest/fee_quoter" + fq16ops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/fee_quoter" + onrampops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/onramp" + fq16 "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_6_0/fee_quoter" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_6_0/offramp" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_6_0/onramp" + offrampops "github.com/smartcontractkit/chainlink-ccip/chains/solana/deployment/v1_6_0/operations/offramp" + deployops "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" + lanesapi "github.com/smartcontractkit/chainlink-ccip/deployment/lanes" + cs_core "github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/mcms" +) + +func TestUpdateToFeeQuoter_1_7(t *testing.T) { + chains := []uint64{ + chain_selectors.ETHEREUM_MAINNET.Selector, + chain_selectors.AVALANCHE_MAINNET.Selector, + } + e, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, chains), + ) + require.NoError(t, err, "Failed to create test environment") + require.NotNil(t, e, "Environment should be created") + mcmsRegistry := cs_core.GetRegistry() + dReg := deployops.GetRegistry() + version := semver.MustParse("1.6.0") + chainInput := make(map[uint64]deployops.ContractDeploymentConfigPerChain) + fqInput := make(map[uint64]deployops.UpdateFeeQuoterInputPerChain) + for _, chainSel := range chains { + chainInput[chainSel] = deployops.ContractDeploymentConfigPerChain{ + Version: version, + // FEE QUOTER CONFIG + MaxFeeJuelsPerMsg: big.NewInt(0).Mul(big.NewInt(200), big.NewInt(1e18)), + TokenPriceStalenessThreshold: uint32(24 * 60 * 60), + LinkPremiumMultiplier: 9e17, // 0.9 ETH + NativeTokenPremiumMultiplier: 1e18, // 1.0 ETH + // OFFRAMP CONFIG + PermissionLessExecutionThresholdSeconds: uint32((20 * time.Minute).Seconds()), + GasForCallExactCheck: uint16(5000), + } + fqInput[chainSel] = deployops.UpdateFeeQuoterInputPerChain{ + ImportFeeQuoterConfigFromVersions: []*semver.Version{ + semver.MustParse("1.6.0"), + }, + FeeQuoterVersion: semver.MustParse("1.7.0"), + RampsVersion: semver.MustParse("1.6.0"), + } + } + out, err := deployops.DeployContracts(dReg).Apply(*e, deployops.ContractDeploymentConfig{ + MCMS: mcms.Input{}, + Chains: chainInput, + }) + require.NoError(t, err, "Failed to apply DeployChainContracts changeset") + require.NoError(t, out.DataStore.Merge(e.DataStore)) + e.DataStore = out.DataStore.Seal() + chain1 := lanesapi.ChainDefinition{ + Selector: chain_selectors.ETHEREUM_MAINNET.Selector, + GasPrice: big.NewInt(1e9), + FeeQuoterDestChainConfig: lanesapi.DefaultFeeQuoterDestChainConfig(true, chain_selectors.ETHEREUM_MAINNET.Selector), + } + chain2 := lanesapi.ChainDefinition{ + Selector: chain_selectors.AVALANCHE_MAINNET.Selector, + GasPrice: big.NewInt(1e9), + FeeQuoterDestChainConfig: lanesapi.DefaultFeeQuoterDestChainConfig(true, chain_selectors.AVALANCHE_MAINNET.Selector), + } + _, err = lanesapi.ConnectChains(lanesapi.GetLaneAdapterRegistry(), mcmsRegistry).Apply(*e, lanesapi.ConnectChainsConfig{ + Lanes: []lanesapi.LaneConfig{ + { + Version: version, + ChainA: chain1, + ChainB: chain2, + }, + }, + }) + require.NoError(t, err, "Failed to apply ConnectChains changeset") + fqReg := deployops.GetFQAndRampUpdaterRegistry() + // now update to FeeQuoter 1.7.0 + fqUpdateChangeset := deployops.UpdateFeeQuoterChangeset(fqReg, nil) + out, err = fqUpdateChangeset.Apply(*e, deployops.UpdateFeeQuoterInput{ + Chains: fqInput, + }) + require.NoError(t, err, "Failed to apply UpdateFeeQuoterChangeset changeset") + // update datastore with changeset output + require.NoError(t, out.DataStore.Merge(e.DataStore), "Failed to merge changeset output datastore") + e.DataStore = out.DataStore.Seal() + for _, chainSel := range chains { + chain := e.BlockChains.EVMChains()[chainSel] + + // get fee quoter address and check config + fq17AddrRefs := e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(chainSel), + datastore.AddressRefByType(datastore.ContractType(fqops.ContractType)), + datastore.AddressRefByVersion(fqops.Version), + ) + require.Len(t, fq17AddrRefs, 1, "Expected exactly 1 FeeQuoter address ref for chain selector %d", chainSel) + fq17Addr := common.HexToAddress(fq17AddrRefs[0].Address) + fq17Contract, err := fee_quoter.NewFeeQuoter(fq17Addr, chain.Client) + require.NoError(t, err, "Failed to instantiate FeeQuoter 1.7 contract for chain selector %d", chainSel) + fq16AddrRefs := e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(chainSel), + datastore.AddressRefByType(datastore.ContractType(fq16ops.ContractType)), + datastore.AddressRefByVersion(fq16ops.Version), + ) + require.Len(t, fq16AddrRefs, 1, "Expected exactly 1 FeeQuoter address ref for version 1.6.3 and chain selector %d", chainSel) + fq16Addr := common.HexToAddress(fq16AddrRefs[0].Address) + // check that the new fee quoter has the same config as the old fee quoter + fq16Contract, err := fq16.NewFeeQuoter(fq16Addr, chain.Client) + require.NoError(t, err, "Failed to instantiate old FeeQuoter 1.6.3 contract for chain selector %d", chainSel) + staticConfig16, err := fq16Contract.GetStaticConfig(nil) + require.NoError(t, err, "Failed to get FeeQuoter config for old contract for chain selector %d", chainSel) + staticConfig17, err := fq17Contract.GetStaticConfig(nil) + require.NoError(t, err, "Failed to get FeeQuoter config for chain selector %d", chainSel) + require.Equal(t, staticConfig16.MaxFeeJuelsPerMsg, staticConfig17.MaxFeeJuelsPerMsg, "MaxFeeJuelsPerMsg should be the same after update for chain selector %d", chainSel) + require.Equal(t, staticConfig16.LinkToken, staticConfig17.LinkToken, "LinkToken address should be the same after update for chain selector %d", chainSel) + updaters16, err := fq16Contract.GetAllAuthorizedCallers(nil) + require.NoError(t, err, "Failed to get FeeQuoter dynamic config for old contract for chain selector %d", chainSel) + updaters17, err := fq17Contract.GetAllAuthorizedCallers(nil) + require.NoError(t, err, "Failed to get FeeQuoter dynamic config for chain selector %d", chainSel) + require.ElementsMatch(t, updaters16, updaters17) + + var remoteChainSelector uint64 + for _, sel := range chains { + if sel != chainSel { + remoteChainSelector = sel + break + } + } + // check the destination chain config for the lane for few elements to make sure it was copied correctly + destChainConfig17, err := fq17Contract.GetDestChainConfig(nil, remoteChainSelector) + require.NoError(t, err, "Failed to get FeeQuoter dest chain config for chain selector %d", chainSel) + destChainConfig16, err := fq16Contract.GetDestChainConfig(nil, remoteChainSelector) + require.NoError(t, err, "Failed to get FeeQuoter dest chain config for old contract for chain selector %d", chainSel) + require.Equal(t, destChainConfig16.IsEnabled, destChainConfig17.IsEnabled, "IsEnabled in dest chain config should be the same after update for chain selector %d", chainSel) + require.Equal(t, destChainConfig16.DefaultTxGasLimit, destChainConfig17.DefaultTxGasLimit, "DefaultTxGasLimit in dest chain config should be the same after update for chain selector %d", chainSel) + + // get the onramp and offramp + onRampAddrRefs := e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(chainSel), + datastore.AddressRefByType(datastore.ContractType(onrampops.ContractType)), + datastore.AddressRefByVersion(onrampops.Version), + ) + require.Len(t, onRampAddrRefs, 1, "Expected exactly 1 OnRamp address ref for chain selector %d", chainSel) + onRampAddr := common.HexToAddress(onRampAddrRefs[0].Address) + offRampAddrRefs := e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(chainSel), + datastore.AddressRefByType(datastore.ContractType(offrampops.ContractType)), + datastore.AddressRefByVersion(offrampops.Version), + ) + require.Len(t, offRampAddrRefs, 1, "Expected exactly 1 OffRamp address ref for chain selector %d", chainSel) + offRampAddr := common.HexToAddress(offRampAddrRefs[0].Address) + onRampContract, err := onramp.NewOnRamp(onRampAddr, chain.Client) + require.NoError(t, err, "Failed to instantiate OnRamp contract for chain selector %d", chainSel) + offRampContract, err := offramp.NewOffRamp(offRampAddr, chain.Client) + require.NoError(t, err, "Failed to instantiate OffRamp contract for chain selector %d", chainSel) + onRampDCfg, err := onRampContract.GetDynamicConfig(nil) + require.NoError(t, err, "Failed to get OnRamp static config for chain selector %d", chainSel) + offRampDCfg, err := offRampContract.GetDynamicConfig(nil) + require.NoError(t, err, "Failed to get OffRamp static config for chain selector %d", chainSel) + require.Equal(t, onRampDCfg.FeeQuoter, fq17Addr, "OnRamp should point to new FeeQuoter after update for chain selector %d", chainSel) + require.Equal(t, offRampDCfg.FeeQuoter, fq17Addr, "OffRamp should point to new FeeQuoter after update for chain selector %d", chainSel) + } +} diff --git a/integration-tests/go.mod b/integration-tests/go.mod index f11e871bc4..dcbe59af74 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -5,6 +5,7 @@ go 1.25.5 replace ( // Make sure we're working with the latest chainlink-ccip github.com/smartcontractkit/chainlink-ccip => .. + github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment => ../ccv/chains/evm/deployment // Make sure we're working with the latest chainlink-ccip/chains/evm/deployment github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment => ../chains/evm/deployment @@ -38,6 +39,11 @@ require ( github.com/stretchr/testify v1.11.1 ) +require ( + github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260206181544-f1613c67d071 + github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment v0.0.0-20260116092715-df6a64d4bf00 +) + require ( github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 438cbfa771..5b46a5cccf 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -680,6 +680,8 @@ github.com/smartcontractkit/chain-selectors v1.0.89 h1:L9oWZGqQXWyTPnC6ODXgu3b0D github.com/smartcontractkit/chain-selectors v1.0.89/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20251024142440-51f2ad2652a2 h1:vGdeMwHO3ow88HvxfhA4DDPYNY0X9jmdux7L83UF/W8= github.com/smartcontractkit/chainlink-aptos v0.0.0-20251024142440-51f2ad2652a2/go.mod h1:iteU0WORHkArACVh/HoY/1bipV4TcNcJdTmom9uIT0E= +github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260206181544-f1613c67d071 h1:64bvvq3x6F8sJD57B8fLFvaVTOSieEiTMqod2Nwo/ZU= +github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260206181544-f1613c67d071/go.mod h1:Gl35ExaFLinqVhp50+Yq1GnMuHb3fnDtZUFPCtcfV3M= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20251021182606-ee6ba95227d7 h1:T4texNFYjny4T4x5eDHo3eF4nJg4MYAkgreNZaLw4oM= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20251021182606-ee6ba95227d7/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= github.com/smartcontractkit/chainlink-common v0.9.6-0.20260114142648-bd9e1b483e96 h1:ZnBBOLyMLJjgQQm7WRJl8sA9Q2RhwagJ+WR62VnA3MY=