Skip to content

Commit d0940fa

Browse files
committed
add Ramp version check
1 parent c63d3dd commit d0940fa

4 files changed

Lines changed: 193 additions & 5 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package adapters
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
9+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
10+
11+
routerops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router"
12+
onrampOpsV15 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/onramp"
13+
onrampOpsV16 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/onramp"
14+
onrampOpsV20 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/onramp"
15+
routerbind "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/router"
16+
datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore"
17+
)
18+
19+
// EVMFeeContractResolver implements fees.FeeContractResolver for EVM chains by
20+
// reading the live Router on the source chain to infer the on-ramp version.
21+
type EVMFeeContractResolver struct{}
22+
23+
// ResolveFeeContractRef discovers the AddressRef of the contract that holds
24+
// token-transfer fee config for a given (src, dst) lane, by:
25+
// 1. Loading the v1.2.0 Router for src from the datastore.
26+
// 2. Calling Router.getOnRamp(dst).
27+
// 3. Reverse-looking-up the returned on-ramp address in the datastore for its
28+
// Type and Version.
29+
// 4. For an EVM2EVMOnRamp (v1.5) the on-ramp itself is the fee contract.
30+
// For an OnRamp (v1.6+) the FeeQuoter — reachable via getDynamicConfig — is
31+
// the fee contract; the v1.6 and v2.0 OnRamp ABIs both expose a FeeQuoter
32+
// field on their dynamic config but the generated bindings differ, so we
33+
// dispatch on the on-ramp's major version.
34+
//
35+
// The Router (v1.2.0) is a stable hub that maps dst chain selectors to live
36+
// on-ramp addresses, so the operator does not need to know which CCIP version
37+
// each lane is on.
38+
func (EVMFeeContractResolver) ResolveFeeContractRef(e cldf.Environment, src uint64, dst uint64) (datastore.AddressRef, error) {
39+
ds := e.DataStore
40+
41+
chain, ok := e.BlockChains.EVMChains()[src]
42+
if !ok {
43+
return datastore.AddressRef{}, fmt.Errorf("EVM chain with selector %d not found", src)
44+
}
45+
46+
routerRef, err := datastore_utils.FindAndFormatRef(ds, datastore.AddressRef{
47+
Type: datastore.ContractType(routerops.ContractType),
48+
Version: routerops.Version,
49+
}, src, datastore_utils.FullRef)
50+
if err != nil {
51+
return datastore.AddressRef{}, fmt.Errorf("failed to find Router (v%s) for src %d: %w", routerops.Version.String(), src, err)
52+
}
53+
if !common.IsHexAddress(routerRef.Address) {
54+
return datastore.AddressRef{}, fmt.Errorf("invalid Router address %q for src %d", routerRef.Address, src)
55+
}
56+
57+
routerContract, err := routerbind.NewRouter(common.HexToAddress(routerRef.Address), chain.Client)
58+
if err != nil {
59+
return datastore.AddressRef{}, fmt.Errorf("failed to bind Router at %s on src %d: %w", routerRef.Address, src, err)
60+
}
61+
62+
onRampAddr, err := routerContract.GetOnRamp(&bind.CallOpts{Context: e.GetContext()}, dst)
63+
if err != nil {
64+
return datastore.AddressRef{}, fmt.Errorf("failed to call Router.getOnRamp(dst=%d) on src %d at %s: %w", dst, src, routerRef.Address, err)
65+
}
66+
if onRampAddr == (common.Address{}) {
67+
return datastore.AddressRef{}, fmt.Errorf("Router.getOnRamp(dst=%d) on src %d returned the zero address (no live lane)", dst, src)
68+
}
69+
70+
onRampRef, err := datastore_utils.FindAndFormatRef(ds, datastore.AddressRef{
71+
Address: onRampAddr.Hex(),
72+
}, src, datastore_utils.FullRef)
73+
if err != nil {
74+
return datastore.AddressRef{}, fmt.Errorf("on-ramp address %s returned by Router.getOnRamp(dst=%d) on src %d is not present in the datastore: %w", onRampAddr.Hex(), dst, src, err)
75+
}
76+
77+
switch onRampRef.Type {
78+
case datastore.ContractType(onrampOpsV15.ContractType):
79+
return onRampRef, nil
80+
81+
case datastore.ContractType(onrampOpsV16.ContractType):
82+
if onRampRef.Version == nil {
83+
return datastore.AddressRef{}, fmt.Errorf("on-ramp at %s on src %d has no Version metadata in datastore", onRampAddr.Hex(), src)
84+
}
85+
86+
var fqAddr common.Address
87+
switch onRampRef.Version.Major() {
88+
case 1:
89+
c, err := onrampOpsV16.NewOnRampContract(onRampAddr, chain.Client)
90+
if err != nil {
91+
return datastore.AddressRef{}, fmt.Errorf("failed to bind v1.6 OnRamp at %s on src %d: %w", onRampAddr.Hex(), src, err)
92+
}
93+
cfg, err := c.GetDynamicConfig(&bind.CallOpts{Context: e.GetContext()})
94+
if err != nil {
95+
return datastore.AddressRef{}, fmt.Errorf("failed to call v1.6 OnRamp.getDynamicConfig at %s on src %d: %w", onRampAddr.Hex(), src, err)
96+
}
97+
fqAddr = cfg.FeeQuoter
98+
case 2:
99+
c, err := onrampOpsV20.NewOnRampContract(onRampAddr, chain.Client)
100+
if err != nil {
101+
return datastore.AddressRef{}, fmt.Errorf("failed to bind v2.0 OnRamp at %s on src %d: %w", onRampAddr.Hex(), src, err)
102+
}
103+
cfg, err := c.GetDynamicConfig(&bind.CallOpts{Context: e.GetContext()})
104+
if err != nil {
105+
return datastore.AddressRef{}, fmt.Errorf("failed to call v2.0 OnRamp.getDynamicConfig at %s on src %d: %w", onRampAddr.Hex(), src, err)
106+
}
107+
fqAddr = cfg.FeeQuoter
108+
default:
109+
return datastore.AddressRef{}, fmt.Errorf("unsupported OnRamp major version %d (%s) at %s on src %d", onRampRef.Version.Major(), onRampRef.Version.String(), onRampAddr.Hex(), src)
110+
}
111+
112+
if fqAddr == (common.Address{}) {
113+
return datastore.AddressRef{}, fmt.Errorf("FeeQuoter address is zero in OnRamp.getDynamicConfig at %s on src %d", onRampAddr.Hex(), src)
114+
}
115+
116+
fqRef, err := datastore_utils.FindAndFormatRef(ds, datastore.AddressRef{
117+
Address: fqAddr.Hex(),
118+
}, src, datastore_utils.FullRef)
119+
if err != nil {
120+
return datastore.AddressRef{}, fmt.Errorf("FeeQuoter address %s reported by OnRamp at %s on src %d is not present in the datastore: %w", fqAddr.Hex(), onRampAddr.Hex(), src, err)
121+
}
122+
return fqRef, nil
123+
124+
default:
125+
return datastore.AddressRef{}, fmt.Errorf("unsupported on-ramp type %q at address %s on src %d", onRampRef.Type, onRampAddr.Hex(), src)
126+
}
127+
}

chains/evm/deployment/v1_0_0/adapters/init.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
chain_selectors "github.com/smartcontractkit/chain-selectors"
66

77
deployapi "github.com/smartcontractkit/chainlink-ccip/deployment/deploy"
8+
feesapi "github.com/smartcontractkit/chainlink-ccip/deployment/fees"
89
tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens"
910
mcmsreaderapi "github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets"
1011
)
@@ -18,4 +19,5 @@ func init() {
1819
deployapi.GetTransferOwnershipRegistry().RegisterAdapter(chain_selectors.FamilyEVM, v, &EVMTransferOwnershipAdapter{})
1920
mcmsreaderapi.GetRegistry().RegisterMCMSReader(chain_selectors.FamilyEVM, &EVMMCMSReader{})
2021
tokensapi.GetTokenAdapterRegistry().RegisterTokenAdapter(chain_selectors.FamilyEVM, v, &EVMTokenBase{})
22+
feesapi.GetFeeContractResolverRegistry().RegisterFeeContractResolver(chain_selectors.FamilyEVM, EVMFeeContractResolver{})
2123
}

deployment/fees/product.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,57 @@ func GetRegistry() *FeeAdapterRegistry {
8181
})
8282
return singletonRegistry
8383
}
84+
85+
// FeeContractResolver discovers, for a given (src, dst) lane, the AddressRef
86+
// of the contract that holds token-transfer fee config — without requiring the
87+
// caller to know which CCIP version that lane is on. Implementations are
88+
// registered per chain family.
89+
type FeeContractResolver interface {
90+
ResolveFeeContractRef(e cldf.Environment, src uint64, dst uint64) (datastore.AddressRef, error)
91+
}
92+
93+
// FeeContractResolverRegistry maintains a per-chain-family map of
94+
// FeeContractResolvers.
95+
type FeeContractResolverRegistry struct {
96+
mu sync.Mutex
97+
m map[string]FeeContractResolver
98+
}
99+
100+
func newFeeContractResolverRegistry() *FeeContractResolverRegistry {
101+
return &FeeContractResolverRegistry{
102+
m: make(map[string]FeeContractResolver),
103+
}
104+
}
105+
106+
// RegisterFeeContractResolver registers the resolver for a chain family. The
107+
// first registration for a given family wins; subsequent calls are ignored.
108+
func (r *FeeContractResolverRegistry) RegisterFeeContractResolver(chainFamily string, resolver FeeContractResolver) {
109+
r.mu.Lock()
110+
defer r.mu.Unlock()
111+
112+
if _, exists := r.m[chainFamily]; !exists {
113+
r.m[chainFamily] = resolver
114+
}
115+
}
116+
117+
// GetFeeContractResolver returns the resolver for a chain family.
118+
func (r *FeeContractResolverRegistry) GetFeeContractResolver(chainFamily string) (FeeContractResolver, bool) {
119+
r.mu.Lock()
120+
defer r.mu.Unlock()
121+
122+
resolver, ok := r.m[chainFamily]
123+
return resolver, ok
124+
}
125+
126+
var (
127+
singletonResolverRegistry *FeeContractResolverRegistry
128+
resolverOnce sync.Once
129+
)
130+
131+
// GetFeeContractResolverRegistry returns the global singleton instance.
132+
func GetFeeContractResolverRegistry() *FeeContractResolverRegistry {
133+
resolverOnce.Do(func() {
134+
singletonResolverRegistry = newFeeContractResolverRegistry()
135+
})
136+
return singletonResolverRegistry
137+
}

deployment/fees/set_token_transfer_fee.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ type SetTokenTransferFeeInput struct {
3838

3939
func SetTokenTransferFee() cldf.ChangeSetV2[SetTokenTransferFeeInput] {
4040
feeRegistry := GetRegistry()
41+
resolverRegistry := GetFeeContractResolverRegistry()
4142
mcmsRegistry := changesets.GetRegistry()
42-
return cldf.CreateChangeSet(makeApply(feeRegistry, mcmsRegistry), makeVerify(feeRegistry, mcmsRegistry))
43+
return cldf.CreateChangeSet(makeApply(feeRegistry, resolverRegistry, mcmsRegistry), makeVerify(feeRegistry, mcmsRegistry))
4344
}
4445

4546
func makeVerify(_ *FeeAdapterRegistry, _ *changesets.MCMSReaderRegistry) func(cldf.Environment, SetTokenTransferFeeInput) error {
@@ -76,8 +77,12 @@ func makeVerify(_ *FeeAdapterRegistry, _ *changesets.MCMSReaderRegistry) func(cl
7677
}
7778
}
7879

79-
func makeApply(feeRegistry *FeeAdapterRegistry, mcmsRegistry *changesets.MCMSReaderRegistry) func(cldf.Environment, SetTokenTransferFeeInput) (cldf.ChangesetOutput, error) {
80+
func makeApply(feeRegistry *FeeAdapterRegistry, resolverRegistry *FeeContractResolverRegistry, mcmsRegistry *changesets.MCMSReaderRegistry) func(cldf.Environment, SetTokenTransferFeeInput) (cldf.ChangesetOutput, error) {
8081
return func(e cldf.Environment, cfg SetTokenTransferFeeInput) (cldf.ChangesetOutput, error) {
82+
if cfg.Version != nil {
83+
e.Logger.Warnf("SetTokenTransferFeeInput.Version is deprecated and ignored; the fee contract version is inferred per-lane from Router.getOnRamp() (got %s)", cfg.Version.String())
84+
}
85+
8186
batchOps := make([]mcms_types.BatchOperation, 0)
8287
reports := make([]cldf_ops.Report[any, any], 0)
8388

@@ -92,9 +97,9 @@ func makeApply(feeRegistry *FeeAdapterRegistry, mcmsRegistry *changesets.MCMSRea
9297
return cldf.ChangesetOutput{}, fmt.Errorf("failed to get chain family for selector %d: %w", src.Selector, err)
9398
}
9499

95-
adapter, exists := feeRegistry.GetFeeAdapter(srcFamily, cfg.Version)
100+
resolver, exists := resolverRegistry.GetFeeContractResolver(srcFamily)
96101
if !exists {
97-
return cldf.ChangesetOutput{}, fmt.Errorf("no fee adapter found for chain family %s and version %s", srcFamily, cfg.Version.String())
102+
return cldf.ChangesetOutput{}, fmt.Errorf("no fee contract resolver registered for chain family %s", srcFamily)
98103
}
99104

100105
// Build version-grouped settings: version -> settings map
@@ -103,7 +108,7 @@ func makeApply(feeRegistry *FeeAdapterRegistry, mcmsRegistry *changesets.MCMSRea
103108
settings := map[uint64]map[string]*TokenTransferFeeArgs{}
104109
for _, dst := range src.Settings {
105110

106-
feeContractRef, err := adapter.GetFeeContractRef(e, src.Selector, dst.Selector)
111+
feeContractRef, err := resolver.ResolveFeeContractRef(e, src.Selector, dst.Selector)
107112
if err != nil {
108113
return cldf.ChangesetOutput{}, fmt.Errorf("failed to get fee contract ref for src %d and dst %d: %w", src.Selector, dst.Selector, err)
109114
}

0 commit comments

Comments
 (0)