diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 2d7f67d23..177bda4b9 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -14,6 +14,7 @@ require ( github.com/lib/pq v1.10.9 github.com/pelletier/go-toml/v2 v2.2.3 github.com/rs/zerolog v1.33.0 + github.com/smartcontractkit/chainlink-ccip v0.0.0-20250212131315-e9b53b05b02a github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250206215114-fb6c3c35e8e3 github.com/smartcontractkit/chainlink-common v0.4.2-0.20250205141137-8f50d72601bb github.com/smartcontractkit/chainlink-solana v1.1.2-0.20250213035259-e727e73f6181 @@ -327,7 +328,6 @@ require ( github.com/slack-go/slack v0.15.0 // indirect github.com/smartcontractkit/chain-selectors v1.0.40 // indirect github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect - github.com/smartcontractkit/chainlink-ccip v0.0.0-20250212131315-e9b53b05b02a // indirect github.com/smartcontractkit/chainlink-data-streams v0.1.1-0.20250128203428-08031923fbe5 // indirect github.com/smartcontractkit/chainlink-feeds v0.1.1 // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250207205350-420ccacab78a // indirect diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index bc0e3eb6e..b14d15b87 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -2,11 +2,13 @@ package relayinterface import ( "context" + "encoding/binary" "testing" "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens" @@ -310,9 +312,15 @@ func TestPDALookups(t *testing.T) { }) t.Run("PDALookup resolves valid PDA with non-address lookup seeds", func(t *testing.T) { seed1 := []byte("test_seed") - seed2 := []byte("another_seed") - - pda, _, err := solana.FindProgramAddress([][]byte{seed1, seed2}, programID) + seed2 := uint64(4) + bufSeed2 := make([]byte, 8) + binary.LittleEndian.PutUint64(bufSeed2, seed2) + seed3 := ccipocr3.ChainSelector(4) + bufSeed3 := make([]byte, 8) + binary.LittleEndian.PutUint64(bufSeed3, uint64(seed3)) + seed4 := ccipocr3.Bytes32(chainwriter.GetRandomPubKey(t).Bytes()) + + pda, _, err := solana.FindProgramAddress([][]byte{seed1, bufSeed2, bufSeed3, seed4[:]}, programID) require.NoError(t, err) expectedMeta := []*solana.AccountMeta{ @@ -329,14 +337,18 @@ func TestPDALookups(t *testing.T) { Seeds: []chainwriter.Seed{ {Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}}}, {Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}}}, + {Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed3", Location: "ccip_chain_selector"}}}, + {Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed4", Location: "ccip_bytes"}}}, }, IsSigner: false, IsWritable: true, }} args := map[string]interface{}{ - "test_seed": seed1, - "another_seed": seed2, + "test_seed": seed1, + "another_seed": seed2, + "ccip_chain_selector": seed3, + "ccip_bytes": seed4, } result, err := pdaLookup.Resolve(ctx, args, nil, client.MultiClient{}) @@ -366,8 +378,10 @@ func TestPDALookups(t *testing.T) { t.Run("PDALookup resolves valid PDA with address lookup seeds", func(t *testing.T) { seed1 := chainwriter.GetRandomPubKey(t) seed2 := chainwriter.GetRandomPubKey(t) + addr3 := chainwriter.GetRandomPubKey(t) + seed3 := ccipocr3.UnknownEncodedAddress(addr3.String()) - pda, _, err := solana.FindProgramAddress([][]byte{seed1.Bytes(), seed2.Bytes()}, programID) + pda, _, err := solana.FindProgramAddress([][]byte{seed1.Bytes(), seed2.Bytes(), addr3.Bytes()}, programID) require.NoError(t, err) expectedMeta := []*solana.AccountMeta{ @@ -384,14 +398,16 @@ func TestPDALookups(t *testing.T) { Seeds: []chainwriter.Seed{ {Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}}}, {Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}}}, + {Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "seed3", Location: "unknown_encoded_address"}}}, }, IsSigner: false, IsWritable: true, }} args := map[string]interface{}{ - "test_seed": seed1, - "another_seed": seed2, + "test_seed": seed1, + "another_seed": seed2, + "unknown_encoded_address": seed3, } result, err := pdaLookup.Resolve(ctx, args, nil, client.MultiClient{}) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 9ba2c1e5f..d1f3b60fc 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -384,7 +384,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error finding transform function: %w", tfErr), debugID) } s.lggr.Debugw("Applying args transformation", "contract", contractName, "method", method) - args, err = transformFunc(ctx, s, args, accounts, toAddress) + args, accounts, err = transformFunc(ctx, s, args, accounts, toAddress) if err != nil { return errorWithDebugID(fmt.Errorf("error transforming args: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 78ef1754d..4eaec79ff 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -778,7 +778,7 @@ func TestChainWriter_CCIPOfframp(t *testing.T) { }, }, ChainSpecificName: "execute", - ArgsTransform: "CCIP", + ArgsTransform: "CCIPExecute", LookupTables: chainwriter.LookupTables{}, Accounts: []chainwriter.Lookup{ {AccountConstant: &chainwriter.AccountConstant{ @@ -822,7 +822,7 @@ func TestChainWriter_CCIPOfframp(t *testing.T) { }, }, ChainSpecificName: "commit", - ArgsTransform: "", + ArgsTransform: "CCIPCommit", LookupTables: chainwriter.LookupTables{}, Accounts: []chainwriter.Lookup{ {AccountConstant: &chainwriter.AccountConstant{ @@ -922,7 +922,7 @@ func TestChainWriter_CCIPOfframp(t *testing.T) { require.NoError(t, submitErr) }) - t.Run("CCIP commit is encoded successfully", func(t *testing.T) { + t.Run("CCIP commit is encoded successfully and ArgsTransform is applied correctly.", func(t *testing.T) { // mock txm txm := txmMocks.NewTxManager(t) // initialize chain writer @@ -963,6 +963,8 @@ func TestChainWriter_CCIPOfframp(t *testing.T) { dec := ag_binary.NewBorshDecoder(payload) err := dec.Decode(&decoded) require.NoError(t, err) + // The CCIPCommit ArgsTransform should remove the last account since no price updates were provided in the report + require.Len(t, tx.Message.Instructions[0].Accounts, 2) return true }), &txID, mock.Anything).Return(nil).Once() diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 669477097..a2d0dbfdd 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -77,10 +77,22 @@ func GetValuesAtLocation(args any, location string) ([][]byte, error) { vals = append(vals, value.Bytes()) case ccipocr3.UnknownAddress: vals = append(vals, value) + case ccipocr3.UnknownEncodedAddress: + decoded, err := solana.PublicKeyFromBase58(string(value)) + if err != nil { + return nil, err + } + vals = append(vals, decoded[:]) case uint64: buf := make([]byte, 8) binary.LittleEndian.PutUint64(buf, value) vals = append(vals, buf) + case ccipocr3.ChainSelector: + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, uint64(value)) + vals = append(vals, buf) + case ccipocr3.Bytes32: + vals = append(vals, value[:]) case [32]uint8: vals = append(vals, value[:]) default: diff --git a/pkg/solana/chainwriter/transform_registry.go b/pkg/solana/chainwriter/transform_registry.go index 79c5b0235..4bdb480a1 100644 --- a/pkg/solana/chainwriter/transform_registry.go +++ b/pkg/solana/chainwriter/transform_registry.go @@ -2,6 +2,7 @@ package chainwriter import ( "context" + "errors" "fmt" "github.com/gagliardetto/solana-go" @@ -26,10 +27,12 @@ type ReportPostTransform struct { TokenIndexes []byte } -func FindTransform(id string) (func(context.Context, *SolanaChainWriterService, any, solana.AccountMetaSlice, string) (any, error), error) { +func FindTransform(id string) (func(context.Context, *SolanaChainWriterService, any, solana.AccountMetaSlice, string) (any, solana.AccountMetaSlice, error), error) { switch id { - case "CCIP": - return CCIPArgsTransform, nil + case "CCIPExecute": + return CCIPExecuteArgsTransform, nil + case "CCIPCommit": + return CCIPCommitAccountTransform, nil default: return nil, fmt.Errorf("transform not found") } @@ -37,11 +40,11 @@ func FindTransform(id string) (func(context.Context, *SolanaChainWriterService, // This Transform function looks up the token pool addresses in the accounts slice and augments the args // with the indexes of the token pool addresses in the accounts slice. -func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args any, accounts solana.AccountMetaSlice, toAddress string) (any, error) { +func CCIPExecuteArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args any, accounts solana.AccountMetaSlice, toAddress string) (any, solana.AccountMetaSlice, error) { // Fetch offramp config to use to fetch the router address offrampProgramConfig, ok := cw.config.Programs[ccipconsts.ContractNameOffRamp] if !ok { - return nil, fmt.Errorf("%s program not found in config", ccipconsts.ContractNameOffRamp) + return nil, nil, fmt.Errorf("%s program not found in config", ccipconsts.ContractNameOffRamp) } // PDA lookup to fetch router address routerAddrLookup := PDALookups{ @@ -61,16 +64,16 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a } accountMetas, err := routerAddrLookup.Resolve(ctx, nil, nil, cw.client) if err != nil { - return nil, fmt.Errorf("failed to fetch the router program address from the reference addresses account: %w", err) + return nil, nil, fmt.Errorf("failed to fetch the router program address from the reference addresses account: %w", err) } if len(accountMetas) != 1 { - return nil, fmt.Errorf("expect 1 address to be returned for router address, received %d: %w", len(accountMetas), err) + return nil, nil, fmt.Errorf("expect 1 address to be returned for router address, received %d: %w", len(accountMetas), err) } // Fetch router config to use to fetch TokenAdminRegistry routerProgramConfig, ok := cw.config.Programs[ccipconsts.ContractNameRouter] if !ok { - return nil, fmt.Errorf("%s program not found in config", ccipconsts.ContractNameRouter) + return nil, nil, fmt.Errorf("%s program not found in config", ccipconsts.ContractNameRouter) } routerAddress := accountMetas[0].PublicKey @@ -101,7 +104,7 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a tableMap, _, err := cw.ResolveLookupTables(ctx, args, TokenPoolLookupTable) if err != nil { - return nil, err + return nil, nil, err } registryTables := tableMap["PoolLookupTable"] tokenPoolAddresses := []solana.PublicKey{} @@ -114,7 +117,7 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a for _, address := range tokenPoolAddresses { if account.PublicKey == address { if i > 255 { - return nil, fmt.Errorf("index %d out of range for uint8", i) + return nil, nil, fmt.Errorf("index %d out of range for uint8", i) } tokenIndexes = append(tokenIndexes, uint8(i)) //nolint:gosec } @@ -122,12 +125,12 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a } if len(tokenIndexes) != len(tokenPoolAddresses) { - return nil, fmt.Errorf("missing token pools in accounts") + return nil, nil, fmt.Errorf("missing token pools in accounts") } argsTyped, ok := args.(ReportPreTransform) if !ok { - return nil, fmt.Errorf("args is not of type ReportPreTransform") + return nil, nil, fmt.Errorf("args is not of type ReportPreTransform") } argsTransformed := ReportPostTransform{ @@ -138,5 +141,24 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a TokenIndexes: tokenIndexes, } - return argsTransformed, nil + return argsTransformed, accounts, nil +} + +// This Transform function trims off the GlobalState account from commit transactions if there are no token or gas price updates +func CCIPCommitAccountTransform(ctx context.Context, cw *SolanaChainWriterService, args any, accounts solana.AccountMetaSlice, toAddress string) (any, solana.AccountMetaSlice, error) { + var tokenPriceVals, gasPriceVals [][]byte + var err error + tokenPriceVals, err = GetValuesAtLocation(args, "Info.TokenPrices.TokenID") + if err != nil && !errors.Is(err, errFieldNotFound) { + return nil, nil, fmt.Errorf("error getting values at location: %w", err) + } + gasPriceVals, err = GetValuesAtLocation(args, "Info.GasPrices.ChainSel") + if err != nil && !errors.Is(err, errFieldNotFound) { + return nil, nil, fmt.Errorf("error getting values at location: %w", err) + } + transformedAccounts := accounts + if len(tokenPriceVals) == 0 && len(gasPriceVals) == 0 { + transformedAccounts = accounts[:len(accounts)-1] + } + return args, transformedAccounts, nil } diff --git a/pkg/solana/chainwriter/transform_registry_test.go b/pkg/solana/chainwriter/transform_registry_test.go new file mode 100644 index 000000000..bbeba440b --- /dev/null +++ b/pkg/solana/chainwriter/transform_registry_test.go @@ -0,0 +1,110 @@ +package chainwriter_test + +import ( + "context" + "testing" + + "github.com/gagliardetto/solana-go" + ccipconsts "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" +) + +func Test_CCIPExecuteArgsTransform(t *testing.T) { + ctx := tests.Context(t) + offrampAddress := chainwriter.GetRandomPubKey(t) + routerAddress := chainwriter.GetRandomPubKey(t) + + // simplified CCIP Config - only IDLs are required for CCIPExecute ArgsTransform + ccipCWConfig := chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + ccipconsts.ContractNameOffRamp: { + IDL: ccipOfframpIDL, + }, + // Requires only the IDL for the CCIPArgsTransform to fetch the TokenAdminRegistry + ccipconsts.ContractNameRouter: { + IDL: ccipRouterIDL, + }, + }, + } + // mock client + rw := clientmocks.NewReaderWriter(t) + mc := *client.NewMultiClient(func(context.Context) (client.ReaderWriter, error) { + return rw, nil + }) + // initialize chain writer + cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), mc, nil, nil, ccipCWConfig) + require.NoError(t, err) + + destTokenAddr := chainwriter.GetRandomPubKey(t) + poolKeys := []solana.PublicKey{destTokenAddr} + poolKeys = append(poolKeys, chainwriter.CreateTestPubKeys(t, 1)...) + + args := chainwriter.ReportPreTransform{ + Info: ccipocr3.ExecuteReportInfo{ + AbstractReports: []ccipocr3.ExecutePluginReportSingleChain{{ + Messages: []ccipocr3.Message{{ + TokenAmounts: []ccipocr3.RampTokenAmount{{ + DestTokenAddress: ccipocr3.UnknownAddress(destTokenAddr.Bytes()), + }}, + }}, + }}, + }, + } + + accounts := []*solana.AccountMeta{{PublicKey: poolKeys[0]}, {PublicKey: poolKeys[1]}} + + t.Run("CCIPExecute ArgsTransform includes token indexes", func(t *testing.T) { + pda, _, err := solana.FindProgramAddress([][]byte{[]byte("token_admin_registry"), destTokenAddr.Bytes()}, routerAddress) + require.NoError(t, err) + + lookupTable := mockTokenAdminRegistryLookupTable(t, rw, pda) + mockFetchRouterAddress(t, rw, routerAddress, offrampAddress) + mockFetchLookupTableAddresses(t, rw, lookupTable, poolKeys) + transformedArgs, newAccounts, err := chainwriter.CCIPExecuteArgsTransform(ctx, cw, args, accounts, offrampAddress.String()) + require.NoError(t, err) + // Accounts should be unchanged + require.Len(t, newAccounts, 2) + typedArgs, ok := transformedArgs.(chainwriter.ReportPostTransform) + require.True(t, ok) + require.NotNil(t, typedArgs.TokenIndexes) + require.Len(t, typedArgs.TokenIndexes, 1) + }) +} + +func Test_CCIPCommitAccountTransform(t *testing.T) { + ctx := tests.Context(t) + offrampAddress := chainwriter.GetRandomPubKey(t) + key1 := chainwriter.GetRandomPubKey(t) + key2 := chainwriter.GetRandomPubKey(t) + t.Run("CCIPCommit ArgsTransform does not affect accounts if token prices exist", func(t *testing.T) { + args := struct { + Info ccipocr3.CommitReportInfo + }{ + Info: ccipocr3.CommitReportInfo{ + TokenPrices: []ccipocr3.TokenPrice{{TokenID: ccipocr3.UnknownEncodedAddress(key1.String())}}, + }, + } + accounts := []*solana.AccountMeta{{PublicKey: key1}, {PublicKey: key2}} + _, newAccounts, err := chainwriter.CCIPCommitAccountTransform(ctx, nil, args, accounts, offrampAddress.String()) + require.NoError(t, err) + require.Len(t, newAccounts, 2) + }) + t.Run("CCIPCommit ArgsTransform removes last account if token and gas prices do not exist", func(t *testing.T) { + args := struct { + Info ccipocr3.CommitReportInfo + }{ + Info: ccipocr3.CommitReportInfo{}, + } + accounts := []*solana.AccountMeta{{PublicKey: key1}, {PublicKey: key2}} + _, newAccounts, err := chainwriter.CCIPCommitAccountTransform(ctx, nil, args, accounts, offrampAddress.String()) + require.NoError(t, err) + require.Len(t, newAccounts, 1) + }) +}