Skip to content

Commit 394025f

Browse files
authored
Merge branch 'develop' into ozep/unsigned-integer-underflow-check
2 parents b531796 + 7ad4e58 commit 394025f

24 files changed

Lines changed: 661 additions & 259 deletions

.github/workflows/ccip-solana-messaging.yml

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,10 @@ jobs:
5353
fail-fast: false
5454
matrix:
5555
test:
56-
- name: EVM2Solana
57-
slug: evm2solana
58-
run: Test_CCIPMessaging_EVM2Solana
59-
loopp: false
6056
- name: EVM2Solana LOOPP
6157
slug: evm2solana-loopp
6258
run: Test_CCIPMessaging_EVM2Solana
6359
loopp: true
64-
- name: Solana2EVM
65-
slug: solana2evm
66-
run: Test_CCIPMessaging_Solana2EVM
67-
loopp: false
6860
- name: Solana2EVM LOOPP
6961
slug: solana2evm-loopp
7062
run: Test_CCIPMessaging_Solana2EVM
@@ -73,10 +65,6 @@ jobs:
7365
slug: revert-evm2solana-loopp
7466
run: Test_CCIPMessaging_Revert_EVM2Solana
7567
loopp: true
76-
- name: MultiExecReports EVM2Solana
77-
slug: multiexec-evm2solana
78-
run: Test_CCIPMessaging_MultiExecReports_EVM2Solana
79-
loopp: false
8068
steps:
8169
- name: Checkout chainlink-solana
8270
uses: actions/checkout@v5
@@ -137,7 +125,6 @@ jobs:
137125
working-directory: chainlink/integration-tests
138126
env:
139127
CL_DATABASE_URL: ${{ steps.setup-postgres.outputs.database-url }}
140-
CL_SOLANA_CMD: ${{ matrix.test.loopp && 'chainlink-solana' || '' }}
141128
run: |
142129
mkdir -p "${GITHUB_WORKSPACE}/ccip-test-artifacts"
143130
cd smoke/ccip

pkg/solana/CONFIG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,18 +371,21 @@ AcceptanceTimeout is the default timeout for a tranmission to be accepted on cha
371371
ForwarderAddress = '14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5' # Example
372372
```
373373
ForwarderAddress is the keystone forwarder program address on chain.
374+
Deprecated: ignored at runtime; use capability config. Kept for TOML compatibility.
374375

375376
### ForwarderState
376377
```toml
377378
ForwarderState = '14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5' # Example
378379
```
379380
ForwarderState is the keystone forwarder program state account on chain.
381+
Deprecated: ignored at runtime; use capability config. Kept for TOML compatibility.
380382

381383
### FromAddress
382384
```toml
383385
FromAddress = '4BJXYkfvg37zEmBbsacZjeQDpTNx91KppxFJxRqrz48e' # Example
384386
```
385387
FromAddress is Address of the transmitter key to use for workflow writes.
388+
Deprecated: ignored at runtime; use capability / transmitter config. Kept for TOML compatibility.
386389

387390
### GasLimitDefault
388391
```toml

pkg/solana/cmd/chainlink-solana/main.go

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ package main
33
import (
44
"context"
55
"fmt"
6-
"strings"
76

87
"github.com/hashicorp/go-plugin"
9-
"github.com/pelletier/go-toml/v2"
108

119
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
1210
"github.com/smartcontractkit/chainlink-common/pkg/loop"
@@ -54,27 +52,21 @@ type pluginRelayer struct {
5452
ds sqlutil.DataSource
5553
}
5654

57-
func (c *pluginRelayer) NewRelayer(ctx context.Context, config string, keystore core.Keystore, csaKeystore core.Keystore, capRegistry core.CapabilitiesRegistry) (loop.Relayer, error) {
58-
d := toml.NewDecoder(strings.NewReader(config))
59-
d.DisallowUnknownFields()
60-
var cfg struct {
61-
Solana solcfg.TOMLConfig
62-
}
63-
64-
if err := d.Decode(&cfg); err != nil {
65-
return nil, fmt.Errorf("failed to decode config toml: %w:\n\t%s", err, config)
55+
func (c *pluginRelayer) NewRelayer(ctx context.Context, rawConfig string, keystore core.Keystore, csaKeystore core.Keystore, capRegistry core.CapabilitiesRegistry) (loop.Relayer, error) {
56+
cfg, err := solcfg.NewDecodedTOMLConfig(rawConfig)
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to read configs: %w", err)
6659
}
67-
68-
rawNodes := make([]map[string]string, 0, len(cfg.Solana.Nodes))
69-
for _, n := range cfg.Solana.Nodes {
60+
rawNodes := make([]map[string]string, 0, len(cfg.Nodes))
61+
for _, n := range cfg.Nodes {
7062
if n == nil || n.URL == nil {
7163
continue
7264
}
7365
rawNodes = append(rawNodes, map[string]string{"URL": n.URL.String()})
7466
}
7567
chainID := ""
76-
if cfg.Solana.ChainID != nil {
77-
chainID = *cfg.Solana.ChainID
68+
if cfg.ChainID != nil {
69+
chainID = *cfg.ChainID
7870
}
7971
emitter := loop.NewPluginRelayerConfigEmitter(
8072
c.Logger,
@@ -93,12 +85,12 @@ func (c *pluginRelayer) NewRelayer(ctx context.Context, config string, keystore
9385
DS: c.ds,
9486
}
9587

96-
chain, err := solana.NewChain(&cfg.Solana, opts)
88+
chain, err := solana.NewChain(cfg, opts)
9789
if err != nil {
9890
return nil, fmt.Errorf("failed to create chain: %w", err)
9991
}
10092

101-
ra := solana.NewRelayer(c.Logger, chain, capRegistry)
93+
ra := solana.NewRelayer(c.Logger, chain, capRegistry, keystore)
10294

10395
c.SubService(ra)
10496

pkg/solana/codec/codec.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import (
1818
func NewEventCodecEntry(filter types.Filter, includeDiscriminator bool, mod commoncodec.Modifier, builder encodings.Builder) (solcommoncodec.Entry, error) {
1919
if filter.ContractIdl != "" {
2020
// Try codecv2 first (Anchor 0.30+ IDL format)
21-
entry, err := codecv2.NewEventArgsEntryWrapper(filter.EventName, filter.ContractIdl, includeDiscriminator, mod, builder)
22-
if err == nil {
21+
entry, v2Err := codecv2.NewEventArgsEntryWrapper(filter.EventName, filter.ContractIdl, includeDiscriminator, mod, builder)
22+
if v2Err == nil {
2323
return entry, nil
2424
}
2525
// Fall back to codec (legacy Anchor IDL format)
26-
entry, err = codecv1.NewEventArgsEntryWrapper(filter.EventName, filter.ContractIdl, includeDiscriminator, mod, builder)
27-
if err != nil {
28-
return nil, fmt.Errorf("failed to create event codec entry: %w", err)
26+
entry, v1Err := codecv1.NewEventArgsEntryWrapper(filter.EventName, filter.ContractIdl, includeDiscriminator, mod, builder)
27+
if v1Err != nil {
28+
return nil, fmt.Errorf("failed to create event codec entry: v2 err: %w, v1 err: %w", v2Err, v1Err)
2929
}
3030
return entry, nil
3131
}

pkg/solana/codec/v1/codec_entry.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66

77
"github.com/smartcontractkit/chainlink-common/pkg/codec"
88
"github.com/smartcontractkit/chainlink-common/pkg/codec/encodings"
9-
"github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary"
9+
1010
solcommoncodec "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec/common"
1111
)
1212

@@ -77,23 +77,35 @@ type EventIDLTypes struct {
7777
Types IdlTypeDefSlice
7878
}
7979

80-
// accepts contract idl string
80+
// NewEventArgsEntryWrapper accepts a contract IDL JSON string and builds a v1 event codec entry.
81+
// It rejects Anchor v2 (0.30+) IDLs, which lack a top-level "version" field, to prevent
82+
// silently producing an empty codec (v2 stores event fields externally in "types").
8183
func NewEventArgsEntryWrapper(offChainName string, contractIdl string, includeDiscriminator bool, mod codec.Modifier, builder encodings.Builder) (solcommoncodec.Entry, error) {
8284
var codecIDL IDL
8385
if err := json.Unmarshal([]byte(contractIdl), &codecIDL); err != nil {
8486
return nil, fmt.Errorf("failed to unmarshal contract IDL for %s, error: %w", offChainName, err)
8587
}
8688

89+
if codecIDL.Version == "" {
90+
return nil, fmt.Errorf("IDL for %s is not a legacy Anchor IDL (missing top-level \"version\" field); use the v2 codec for Anchor 0.30+ IDLs", offChainName)
91+
}
92+
8793
eventIdl, err := ExtractEventIDL(offChainName, codecIDL)
8894
if err != nil {
8995
return nil, fmt.Errorf("failed to extract event IDL from codec: %w", err)
9096
}
9197

92-
return NewEventArgsEntry(offChainName, EventIDLTypes{Event: eventIdl, Types: codecIDL.Types}, true, nil, binary.LittleEndian())
98+
return NewEventArgsEntry(offChainName, EventIDLTypes{Event: eventIdl, Types: codecIDL.Types}, includeDiscriminator, mod, builder)
9399
}
94100

95-
// accepts struct containing event definition and types parsed from the contract idl
101+
// NewEventArgsEntry accepts a struct containing event definition and types parsed from the contract IDL.
102+
// It requires the event to have at least one field to prevent building an empty codec
103+
// that would silently discard decoded data.
96104
func NewEventArgsEntry(offChainName string, idlTypes EventIDLTypes, includeDiscriminator bool, mod codec.Modifier, builder encodings.Builder) (solcommoncodec.Entry, error) {
105+
if len(idlTypes.Event.Fields) == 0 {
106+
return nil, fmt.Errorf("event %q has no fields; this may indicate an Anchor v2 IDL was parsed with the v1 codec", idlTypes.Event.Name)
107+
}
108+
97109
_, eventCodec, err := asStruct(eventFieldsToFields(idlTypes.Event.Fields), createRefs(idlTypes.Types, builder), idlTypes.Event.Name, false, false)
98110
if err != nil {
99111
return nil, err

pkg/solana/codec/v1/solana_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,35 @@ func TestNewIDLCodec_WithModifiers(t *testing.T) {
152152
require.Equal(t, expected.EnumVal, unmodifiedDecoded.EnumVal)
153153
}
154154

155+
func TestNewEventArgsEntryWrapper_RejectsV2IDL(t *testing.T) {
156+
t.Parallel()
157+
158+
// Minimal v2-style IDL: has events without inline fields and no top-level "version".
159+
// This would unmarshal successfully into the v1 IDL struct, producing an event
160+
// with empty Fields that generates an empty codec — the exact vulnerability.
161+
v2StyleIDL := `{
162+
"name": "test_program",
163+
"instructions": [],
164+
"events": [{"name": "TestItem", "discriminator": [119, 183, 160, 247, 84, 104, 222, 251]}],
165+
"types": [{"name": "TestItem", "type": {"kind": "struct", "fields": [{"name": "field", "type": "i32"}]}}]
166+
}`
167+
168+
_, err := codecv1.NewEventArgsEntryWrapper("TestItem", v2StyleIDL, true, nil, binary.LittleEndian())
169+
require.Error(t, err)
170+
assert.Contains(t, err.Error(), "not a legacy Anchor IDL")
171+
}
172+
173+
func TestNewEventArgsEntry_RejectsEmptyFields(t *testing.T) {
174+
t.Parallel()
175+
176+
_, err := codecv1.NewEventArgsEntry("TestEmpty", codecv1.EventIDLTypes{
177+
Event: codecv1.IdlEvent{Name: "TestEmpty", Fields: nil},
178+
Types: nil,
179+
}, true, nil, binary.LittleEndian())
180+
require.Error(t, err)
181+
assert.Contains(t, err.Error(), "has no fields")
182+
}
183+
155184
func TestNewIDLCodec_CircularDependency(t *testing.T) {
156185
t.Parallel()
157186

pkg/solana/codec/v2/codec_entry.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88

99
"github.com/smartcontractkit/chainlink-common/pkg/codec"
1010
"github.com/smartcontractkit/chainlink-common/pkg/codec/encodings"
11-
"github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary"
1211

1312
solcommoncodec "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec/common"
1413
)
@@ -53,7 +52,7 @@ func NewEventArgsEntryWrapper(offChainName string, contractIdl string, includeDi
5352
return nil, fmt.Errorf("failed to extract event IDL from codec: %w", err)
5453
}
5554

56-
return NewEventArgsEntry(offChainName, EventIDLTypes{Event: eventIdl, Types: codecIDL.Types}, true, nil, binary.LittleEndian())
55+
return NewEventArgsEntry(offChainName, EventIDLTypes{Event: eventIdl, Types: codecIDL.Types}, includeDiscriminator, mod, builder)
5756
}
5857

5958
func NewEventArgsEntry(offChainName string, idlTypes EventIDLTypes, includeDiscriminator bool, mod codec.Modifier, builder encodings.Builder) (solcommoncodec.Entry, error) {

pkg/solana/codec/v2/codec_entry_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package codecv2_test
1+
package codecv2
22

33
import (
44
_ "embed"
@@ -10,7 +10,6 @@ import (
1010
"github.com/stretchr/testify/require"
1111

1212
"github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary"
13-
codecv2 "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec/v2"
1413
)
1514

1615
//go:embed testutils/data_storage.json
@@ -41,7 +40,7 @@ func TestNewEventArgsEntry(t *testing.T) {
4140
t.Fatalf("unexpected error: invalid IDL, error: %v", err)
4241
}
4342
for i, event := range idl.Events {
44-
entry, err := codecv2.NewEventArgsEntry(fmt.Sprintf("test%d", i), codecv2.EventIDLTypes{
43+
entry, err := NewEventArgsEntry(fmt.Sprintf("test%d", i), EventIDLTypes{
4544
Event: event,
4645
Types: idl.Types,
4746
}, false, nil, binary.LittleEndian())
@@ -59,7 +58,7 @@ func TestNewInstructionArgsEntry(t *testing.T) {
5958
}
6059
for i, instruction := range idl.Instructions {
6160
if len(instruction.Args) > 0 {
62-
entry, err := codecv2.NewInstructionArgsEntry(fmt.Sprintf("test%d", i), codecv2.InstructionArgsIDLTypes{
61+
entry, err := NewInstructionArgsEntry(fmt.Sprintf("test%d", i), InstructionArgsIDLTypes{
6362
Instruction: instruction,
6463
Types: idl.Types,
6564
}, nil, binary.LittleEndian())

pkg/solana/codec/v2/solana.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,31 @@ const (
2424

2525
// snakeToPascal converts a snake_case string to PascalCase.
2626
// e.g. "transmission_id" -> "TransmissionId", "don_id" -> "DonId"
27-
func snakeToPascal(s string) string {
28-
caser := cases.Title(language.English, cases.NoLower)
27+
// Returns an error if the conversion produces an empty string (e.g. underscore-only inputs).
28+
func snakeToPascal(s string) (string, error) {
29+
caser := cases.Title(language.English)
2930
parts := strings.Split(s, "_")
30-
for i, part := range parts {
31+
var result strings.Builder
32+
for _, part := range parts {
3133
if len(part) > 0 {
32-
parts[i] = caser.String(part)
34+
result.WriteString(caser.String(part))
3335
}
3436
}
35-
return strings.Join(parts, "")
37+
if result.Len() == 0 {
38+
return "", fmt.Errorf("%w: field name %q converts to an empty PascalCase string", commontypes.ErrInvalidConfig, s)
39+
}
40+
return result.String(), nil
41+
}
42+
43+
func validateUniqueFieldNames(named []commonencodings.NamedTypeCodec) error {
44+
seen := make(map[string]struct{}, len(named))
45+
for _, n := range named {
46+
if _, exists := seen[n.Name]; exists {
47+
return fmt.Errorf("%w: duplicate PascalCase field name %q", commontypes.ErrInvalidConfig, n.Name)
48+
}
49+
seen[n.Name] = struct{}{}
50+
}
51+
return nil
3652
}
3753

3854
func CreateCodecEntry(idlDefinition interface{}, offChainName string, idl anchoridl.Idl, mod commoncodec.Modifier) (entry solcommoncodec.Entry, err error) {
@@ -163,7 +179,14 @@ func asStruct(
163179
return name, nil, err
164180
}
165181

166-
named[idx] = commonencodings.NamedTypeCodec{Name: snakeToPascal(fieldName), Codec: typedCodec}
182+
pascalName, err := snakeToPascal(fieldName)
183+
if err != nil {
184+
return name, nil, err
185+
}
186+
named[idx] = commonencodings.NamedTypeCodec{Name: pascalName, Codec: typedCodec}
187+
}
188+
if err := validateUniqueFieldNames(named); err != nil {
189+
return name, nil, err
167190
}
168191
structCodec, err := commonencodings.NewStructCodec(named)
169192
if err != nil {
@@ -189,7 +212,11 @@ func asStructForInstructionArgs(
189212
if err != nil {
190213
return nil, err
191214
}
192-
named[idx] = commonencodings.NamedTypeCodec{Name: snakeToPascal(fieldName), Codec: typedCodec}
215+
pascalName, err := snakeToPascal(fieldName)
216+
if err != nil {
217+
return nil, err
218+
}
219+
named[idx] = commonencodings.NamedTypeCodec{Name: pascalName, Codec: typedCodec}
193220
}
194221

195222
var isVecOrArray bool
@@ -209,6 +236,10 @@ func asStructForInstructionArgs(
209236
return named[0].Codec, nil
210237
}
211238

239+
if err := validateUniqueFieldNames(named); err != nil {
240+
return nil, err
241+
}
242+
212243
structCodec, err := commonencodings.NewStructCodec(named)
213244
if err != nil {
214245
return nil, err

0 commit comments

Comments
 (0)