Skip to content

Commit 047195d

Browse files
committed
general improvements
1 parent b90826a commit 047195d

4 files changed

Lines changed: 247 additions & 37 deletions

File tree

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

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
routerops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router"
1616
routerbind "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/router"
17+
feesapi "github.com/smartcontractkit/chainlink-ccip/deployment/fees"
1718
cciputils "github.com/smartcontractkit/chainlink-ccip/deployment/utils"
1819
datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore"
1920
)
@@ -57,6 +58,15 @@ func newEVMFeeContractResolver() *EVMFeeContractResolver {
5758
// Patch components of the version are stripped before keying, so 1.6.x all map
5859
// to the same Ops. First registration wins.
5960
func (r *EVMFeeContractResolver) RegisterOnRampOps(t datastore.ContractType, v *semver.Version, ops OnRampFeeContractOps) {
61+
if t == "" {
62+
panic("RegisterOnRampOps: empty ContractType")
63+
}
64+
if v == nil {
65+
panic(fmt.Sprintf("RegisterOnRampOps: nil version for type=%q", t))
66+
}
67+
if ops == nil {
68+
panic(fmt.Sprintf("RegisterOnRampOps: nil ops for type=%q version=%s", t, v.String()))
69+
}
6070
key := newOnRampOpsKey(t, v)
6171
r.mu.Lock()
6272
defer r.mu.Unlock()
@@ -65,24 +75,30 @@ func (r *EVMFeeContractResolver) RegisterOnRampOps(t datastore.ContractType, v *
6575
}
6676
}
6777

78+
// ResolveFeeContractRef returns the AddressRef of the contract that holds
79+
// token-transfer fee config for the (src, dst) lane. Callers select the
80+
// per-version FeeAdapter from the returned AddressRef.Version after
81+
// StripPatchVersion; the returned Address is informational because each
82+
// adapter re-derives its own write target.
6883
func (r *EVMFeeContractResolver) ResolveFeeContractRef(e cldf.Environment, src uint64, dst uint64) (datastore.AddressRef, error) {
6984
ds := e.DataStore
7085

7186
chain, ok := e.BlockChains.EVMChains()[src]
7287
if !ok {
7388
return datastore.AddressRef{}, fmt.Errorf("EVM chain with selector %d not found", src)
7489
}
90+
if chain.Client == nil {
91+
return datastore.AddressRef{}, fmt.Errorf("EVM chain %d has nil Client; cannot read live Router state", src)
92+
}
7593

76-
routerRef, err := datastore_utils.FindAndFormatRef(ds, datastore.AddressRef{
77-
Type: datastore.ContractType(routerops.ContractType),
78-
Version: routerops.Version,
79-
}, src, datastore_utils.FullRef)
94+
routerRef, err := findRouterRef(ds, src)
8095
if err != nil {
81-
return datastore.AddressRef{}, fmt.Errorf("failed to find Router (v%s) for src %d: %w", routerops.Version.String(), src, err)
96+
return datastore.AddressRef{}, err
8297
}
8398
if !common.IsHexAddress(routerRef.Address) {
8499
return datastore.AddressRef{}, fmt.Errorf("invalid Router address %q for src %d", routerRef.Address, src)
85100
}
101+
e.Logger.Infof("EVMFeeContractResolver: src=%d using %s at %s (v%s) to resolve OnRamp for dst=%d", src, routerRef.Type, routerRef.Address, routerRef.Version.String(), dst)
86102

87103
rc, err := routerbind.NewRouter(common.HexToAddress(routerRef.Address), chain.Client)
88104
if err != nil {
@@ -91,51 +107,90 @@ func (r *EVMFeeContractResolver) ResolveFeeContractRef(e cldf.Environment, src u
91107

92108
onRampAddr, err := rc.GetOnRamp(&bind.CallOpts{Context: e.GetContext()}, dst)
93109
if err != nil {
94-
return datastore.AddressRef{}, fmt.Errorf("failed to call Router.getOnRamp(dst=%d) on src %d at %s: %w", dst, src, routerRef.Address, err)
110+
return datastore.AddressRef{}, fmt.Errorf("failed to call Router.GetOnRamp(dst=%d) on src %d at %s: %w", dst, src, routerRef.Address, err)
95111
}
96112
if onRampAddr == (common.Address{}) {
97-
return datastore.AddressRef{}, fmt.Errorf("Router.getOnRamp(dst=%d) on src %d returned the zero address (no live lane)", dst, src)
113+
return datastore.AddressRef{}, fmt.Errorf("Router.GetOnRamp(dst=%d) on src %d at %s returned the zero address: %w", dst, src, routerRef.Address, feesapi.ErrNoLiveLane)
98114
}
99115

100116
onRampRef, err := datastore_utils.FindAndFormatRef(ds, datastore.AddressRef{
101117
Address: onRampAddr.Hex(),
102118
}, src, datastore_utils.FullRef)
103119
if err != nil {
104-
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)
120+
return datastore.AddressRef{}, fmt.Errorf("OnRamp address %s returned by Router.GetOnRamp(dst=%d) on src %d is not present in the datastore: %w", onRampAddr.Hex(), dst, src, err)
105121
}
106122
if onRampRef.Version == nil {
107-
return datastore.AddressRef{}, fmt.Errorf("on-ramp at %s on src %d has no Version metadata in datastore", onRampAddr.Hex(), src)
123+
return datastore.AddressRef{}, fmt.Errorf("OnRamp at %s on src %d has no Version metadata in datastore", onRampAddr.Hex(), src)
108124
}
109125

110126
key := newOnRampOpsKey(onRampRef.Type, onRampRef.Version)
111127
r.mu.RLock()
112128
ops, ok := r.onRamps[key]
113129
r.mu.RUnlock()
114130
if !ok {
115-
return datastore.AddressRef{}, fmt.Errorf("no OnRampFeeContractOps registered for type=%q version=%s", onRampRef.Type, onRampRef.Version.String())
131+
return datastore.AddressRef{}, fmt.Errorf(
132+
"no OnRampFeeContractOps registered for type=%q version=%s (lookup key %s) at OnRamp %s on src %d, dst %d",
133+
onRampRef.Type,
134+
onRampRef.Version.String(),
135+
cciputils.StripPatchVersion(onRampRef.Version).String(),
136+
onRampAddr.Hex(),
137+
src,
138+
dst,
139+
)
116140
}
117141

118142
feeContractAddr, err := ops.GetFeeContractAddress(e.GetContext(), chain, onRampAddr)
119143
if err != nil {
120-
return datastore.AddressRef{}, fmt.Errorf("failed to resolve fee-contract address for on-ramp %s on src %d: %w", onRampAddr.Hex(), src, err)
144+
return datastore.AddressRef{}, fmt.Errorf("failed to resolve fee-contract address for OnRamp %s on src %d: %w", onRampAddr.Hex(), src, err)
121145
}
122146
if feeContractAddr == (common.Address{}) {
123-
return datastore.AddressRef{}, fmt.Errorf("OnRampFeeContractOps returned the zero address for on-ramp %s on src %d", onRampAddr.Hex(), src)
147+
return datastore.AddressRef{}, fmt.Errorf("OnRampFeeContractOps returned the zero address for OnRamp %s on src %d", onRampAddr.Hex(), src)
124148
}
125149

150+
// v1.5 short-circuit: the EVM2EVMOnRamp itself holds fee config, so the
151+
// onramp ref already points at the fee contract. Skip the second datastore
152+
// lookup; the returned ref's Type will be the OnRamp's type
153+
// (e.g. EVM2EVMOnRamp), which is intentional for v1.5.
126154
if feeContractAddr == onRampAddr {
127155
return onRampRef, nil
128156
}
129157

130158
feeRef, err := datastore_utils.FindAndFormatRef(ds, datastore.AddressRef{
159+
Type: datastore.ContractType(cciputils.FeeQuoter),
131160
Address: feeContractAddr.Hex(),
132161
}, src, datastore_utils.FullRef)
133162
if err != nil {
134-
return datastore.AddressRef{}, fmt.Errorf("fee-contract address %s reported by OnRamp at %s on src %d is not present in the datastore: %w", feeContractAddr.Hex(), onRampAddr.Hex(), src, err)
163+
return datastore.AddressRef{}, fmt.Errorf("FeeQuoter address %s reported by OnRamp at %s on src %d is not present in the datastore (filtered by Type=FeeQuoter): %w", feeContractAddr.Hex(), onRampAddr.Hex(), src, err)
164+
}
165+
if feeRef.Version == nil {
166+
return datastore.AddressRef{}, fmt.Errorf("FeeQuoter at %s on src %d (reported by OnRamp %s) has no Version metadata in datastore", feeContractAddr.Hex(), src, onRampAddr.Hex())
135167
}
136168
return feeRef, nil
137169
}
138170

171+
// findRouterRef looks up the active Router for src in the datastore, falling
172+
// back to TestRouter when the production Router is not registered. Production
173+
// is preferred when both exist; this matches the broader codebase pattern of
174+
// keeping Router and TestRouter as separate ContractTypes that callers select
175+
// between explicitly (see v1_6_0/sequences/adapter.go GetRouter / GetTestRouter).
176+
func findRouterRef(ds datastore.DataStore, src uint64) (datastore.AddressRef, error) {
177+
ref, err := datastore_utils.FindAndFormatRef(ds, datastore.AddressRef{
178+
Type: datastore.ContractType(routerops.ContractType),
179+
Version: routerops.Version,
180+
}, src, datastore_utils.FullRef)
181+
if err == nil {
182+
return ref, nil
183+
}
184+
testRef, testErr := datastore_utils.FindAndFormatRef(ds, datastore.AddressRef{
185+
Type: datastore.ContractType(routerops.TestRouterContractType),
186+
Version: routerops.Version,
187+
}, src, datastore_utils.FullRef)
188+
if testErr == nil {
189+
return testRef, nil
190+
}
191+
return datastore.AddressRef{}, fmt.Errorf("no Router or TestRouter (v%s) for src %d: router lookup error: %w; testRouter lookup error: %v", routerops.Version.String(), src, err, testErr)
192+
}
193+
139194
var (
140195
evmFeeContractResolverOnce sync.Once
141196
evmFeeContractResolver *EVMFeeContractResolver
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package adapters
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/Masterminds/semver/v3"
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
10+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// stubOps is a no-op OnRampFeeContractOps used to exercise registration
15+
// behavior without standing up a chain. The returned address is fixed.
16+
type stubOps struct {
17+
ret common.Address
18+
}
19+
20+
func (s stubOps) GetFeeContractAddress(_ context.Context, _ evm.Chain, _ common.Address) (common.Address, error) {
21+
return s.ret, nil
22+
}
23+
24+
const fakeOnRampType datastore.ContractType = "FakeOnRampForTest"
25+
26+
func TestRegisterOnRampOps_PanicsOnEmptyType(t *testing.T) {
27+
t.Parallel()
28+
r := newEVMFeeContractResolver()
29+
require.PanicsWithValue(t, "RegisterOnRampOps: empty ContractType", func() {
30+
r.RegisterOnRampOps("", semver.MustParse("1.0.0"), stubOps{})
31+
})
32+
}
33+
34+
func TestRegisterOnRampOps_PanicsOnNilVersion(t *testing.T) {
35+
t.Parallel()
36+
r := newEVMFeeContractResolver()
37+
require.Panics(t, func() {
38+
r.RegisterOnRampOps(fakeOnRampType, nil, stubOps{})
39+
})
40+
}
41+
42+
func TestRegisterOnRampOps_PanicsOnNilOps(t *testing.T) {
43+
t.Parallel()
44+
r := newEVMFeeContractResolver()
45+
require.Panics(t, func() {
46+
r.RegisterOnRampOps(fakeOnRampType, semver.MustParse("1.0.0"), nil)
47+
})
48+
}
49+
50+
func TestRegisterOnRampOps_FirstWins(t *testing.T) {
51+
t.Parallel()
52+
r := newEVMFeeContractResolver()
53+
first := stubOps{ret: common.HexToAddress("0x1111111111111111111111111111111111111111")}
54+
second := stubOps{ret: common.HexToAddress("0x2222222222222222222222222222222222222222")}
55+
56+
r.RegisterOnRampOps(fakeOnRampType, semver.MustParse("1.0.0"), first)
57+
r.RegisterOnRampOps(fakeOnRampType, semver.MustParse("1.0.0"), second)
58+
59+
got := r.onRamps[newOnRampOpsKey(fakeOnRampType, semver.MustParse("1.0.0"))]
60+
registered, ok := got.(stubOps)
61+
require.True(t, ok, "registered Ops must be stubOps")
62+
require.Equal(t, first.ret, registered.ret, "first registration must win")
63+
}
64+
65+
func TestRegisterOnRampOps_PatchStrippingMergesKeys(t *testing.T) {
66+
t.Parallel()
67+
r := newEVMFeeContractResolver()
68+
v160 := stubOps{ret: common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}
69+
v163 := stubOps{ret: common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")}
70+
71+
r.RegisterOnRampOps(fakeOnRampType, semver.MustParse("1.6.0"), v160)
72+
// 1.6.3 strips to 1.6.0 → same key, first-wins semantics keep v160.
73+
r.RegisterOnRampOps(fakeOnRampType, semver.MustParse("1.6.3"), v163)
74+
75+
require.Len(t, r.onRamps, 1, "patch-version variants must collapse to one map entry")
76+
77+
got := r.onRamps[newOnRampOpsKey(fakeOnRampType, semver.MustParse("1.6.0"))]
78+
require.Equal(t, v160.ret, got.(stubOps).ret)
79+
80+
gotPatch := r.onRamps[newOnRampOpsKey(fakeOnRampType, semver.MustParse("1.6.3"))]
81+
require.Equal(t, v160.ret, gotPatch.(stubOps).ret, "1.6.3 must resolve to the same Ops as 1.6.0")
82+
}
83+
84+
func TestNewOnRampOpsKey_StripsPatch(t *testing.T) {
85+
t.Parallel()
86+
require.Equal(t,
87+
newOnRampOpsKey(fakeOnRampType, semver.MustParse("1.6.0")),
88+
newOnRampOpsKey(fakeOnRampType, semver.MustParse("1.6.3")),
89+
)
90+
require.Equal(t,
91+
newOnRampOpsKey(fakeOnRampType, semver.MustParse("2.0.0")),
92+
newOnRampOpsKey(fakeOnRampType, semver.MustParse("2.0.99")),
93+
)
94+
require.NotEqual(t,
95+
newOnRampOpsKey(fakeOnRampType, semver.MustParse("1.6.0")),
96+
newOnRampOpsKey(fakeOnRampType, semver.MustParse("1.7.0")),
97+
"different minors must not collide",
98+
)
99+
}
100+
101+
func TestEVMFeeContractResolver_Singleton(t *testing.T) {
102+
a := GetEVMFeeContractResolver()
103+
b := GetEVMFeeContractResolver()
104+
require.Same(t, a, b, "GetEVMFeeContractResolver must return the same instance across calls")
105+
require.NotNil(t, a)
106+
}

deployment/fees/product.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fees
22

33
import (
4+
"errors"
45
"fmt"
56
"sync"
67

@@ -82,6 +83,14 @@ func GetRegistry() *FeeAdapterRegistry {
8283
return singletonRegistry
8384
}
8485

86+
// ErrNoLiveLane is returned (wrapped) by a FeeContractResolver implementation
87+
// when the (src, dst) lane has no live router-level mapping — e.g. for EVM,
88+
// when Router.GetOnRamp(dst) yields the zero address. Callers may use
89+
// errors.Is to detect this and fall back to a non-Router discovery path
90+
// (e.g. the legacy FeeAdapter.GetFeeContractRef using a supplied Version)
91+
// when configuring fees before a lane is wired.
92+
var ErrNoLiveLane = errors.New("no live lane on the source chain's router for the given dst")
93+
8594
// FeeContractResolver discovers, for a given (src, dst) lane, the AddressRef
8695
// of the contract that holds token-transfer fee config — without requiring the
8796
// caller to know which CCIP version that lane is on. Implementations are

0 commit comments

Comments
 (0)