From 4653b46c8e95eae7ea5c47d279bc1c36c34148df Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 18 Nov 2024 23:50:08 -0600 Subject: [PATCH] avoid endless rescan when the inbound vote message is invalid --- x/crosschain/types/message_vote_inbound.go | 2 +- zetaclient/chains/bitcoin/observer/event.go | 31 ++--- .../chains/bitcoin/observer/event_test.go | 15 ++- zetaclient/chains/bitcoin/observer/inbound.go | 17 ++- .../chains/bitcoin/observer/inbound_test.go | 4 +- zetaclient/chains/solana/observer/inbound.go | 61 +++++++-- .../chains/solana/observer/inbound_test.go | 82 +++++++++--- zetaclient/compliance/compliance.go | 24 ---- zetaclient/types/event.go | 68 ++++++++++ zetaclient/types/event_test.go | 124 ++++++++++++++++++ 10 files changed, 344 insertions(+), 84 deletions(-) create mode 100644 zetaclient/types/event_test.go diff --git a/x/crosschain/types/message_vote_inbound.go b/x/crosschain/types/message_vote_inbound.go index 3db9fdde0f..e612fe582a 100644 --- a/x/crosschain/types/message_vote_inbound.go +++ b/x/crosschain/types/message_vote_inbound.go @@ -22,7 +22,7 @@ const MaxMessageLength = 10240 // InboundVoteOption is a function that sets some option on the inbound vote message type InboundVoteOption func(*MsgVoteInbound) -// WithMemoRevertOptions sets the revert options for inbound vote message +// WithRevertOptions sets the revert options for inbound vote message func WithRevertOptions(revertOptions RevertOptions) InboundVoteOption { return func(msg *MsgVoteInbound) { msg.RevertOptions = revertOptions diff --git a/zetaclient/chains/bitcoin/observer/event.go b/zetaclient/chains/bitcoin/observer/event.go index 69657d29f1..ffc79916d7 100644 --- a/zetaclient/chains/bitcoin/observer/event.go +++ b/zetaclient/chains/bitcoin/observer/event.go @@ -19,20 +19,7 @@ import ( "github.com/zeta-chain/node/zetaclient/compliance" "github.com/zeta-chain/node/zetaclient/config" "github.com/zeta-chain/node/zetaclient/logs" -) - -// InboundProcessability is an enum representing the processability of an inbound -type InboundProcessability int - -const ( - // InboundProcessabilityGood represents a processable inbound - InboundProcessabilityGood InboundProcessability = iota - - // InboundProcessabilityDonation represents a donation inbound - InboundProcessabilityDonation - - // InboundProcessabilityComplianceViolation represents a compliance violation - InboundProcessabilityComplianceViolation + clienttypes "github.com/zeta-chain/node/zetaclient/types" ) // BTCInboundEvent represents an incoming transaction event @@ -63,10 +50,10 @@ type BTCInboundEvent struct { } // Processability returns the processability of the inbound event -func (event *BTCInboundEvent) Processability() InboundProcessability { +func (event *BTCInboundEvent) Processability() clienttypes.InboundProcessability { // compliance check on sender and receiver addresses if config.ContainRestrictedAddress(event.FromAddress, event.ToAddress) { - return InboundProcessabilityComplianceViolation + return clienttypes.InboundProcessabilityComplianceViolation } // compliance check on receiver, revert/abort addresses in standard memo @@ -76,16 +63,16 @@ func (event *BTCInboundEvent) Processability() InboundProcessability { event.MemoStd.RevertOptions.RevertAddress, event.MemoStd.RevertOptions.AbortAddress, ) { - return InboundProcessabilityComplianceViolation + return clienttypes.InboundProcessabilityComplianceViolation } } // donation check if bytes.Equal(event.MemoBytes, []byte(constant.DonationMessage)) { - return InboundProcessabilityDonation + return clienttypes.InboundProcessabilityDonation } - return InboundProcessabilityGood + return clienttypes.InboundProcessabilityGood } // DecodeMemoBytes decodes the contained memo bytes as either standard or legacy memo @@ -168,16 +155,16 @@ func ValidateStandardMemo(memoStd memo.InboundMemo, chainID int64) error { func (ob *Observer) CheckEventProcessability(event BTCInboundEvent) bool { // check if the event is processable switch result := event.Processability(); result { - case InboundProcessabilityGood: + case clienttypes.InboundProcessabilityGood: return true - case InboundProcessabilityDonation: + case clienttypes.InboundProcessabilityDonation: logFields := map[string]any{ logs.FieldChain: ob.Chain().ChainId, logs.FieldTx: event.TxHash, } ob.Logger().Inbound.Info().Fields(logFields).Msgf("thank you rich folk for your donation!") return false - case InboundProcessabilityComplianceViolation: + case clienttypes.InboundProcessabilityComplianceViolation: compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, false, ob.Chain().ChainId, event.TxHash, event.FromAddress, event.ToAddress, "BTC") return false diff --git a/zetaclient/chains/bitcoin/observer/event_test.go b/zetaclient/chains/bitcoin/observer/event_test.go index 5ed8e9b103..fd870731f8 100644 --- a/zetaclient/chains/bitcoin/observer/event_test.go +++ b/zetaclient/chains/bitcoin/observer/event_test.go @@ -22,6 +22,7 @@ import ( "github.com/zeta-chain/node/zetaclient/keys" "github.com/zeta-chain/node/zetaclient/testutils" "github.com/zeta-chain/node/zetaclient/testutils/mocks" + clienttypes "github.com/zeta-chain/node/zetaclient/types" ) // createTestBtcEvent creates a test BTC inbound event @@ -41,7 +42,7 @@ func createTestBtcEvent( } } -func Test_CheckProcessability(t *testing.T) { +func Test_Processability(t *testing.T) { // setup compliance config cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), @@ -52,7 +53,7 @@ func Test_CheckProcessability(t *testing.T) { tests := []struct { name string event *observer.BTCInboundEvent - expected observer.InboundProcessability + expected clienttypes.InboundProcessability }{ { name: "should return InboundProcessabilityGood for a processable inbound event", @@ -60,7 +61,7 @@ func Test_CheckProcessability(t *testing.T) { FromAddress: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", ToAddress: testutils.TSSAddressBTCAthens3, }, - expected: observer.InboundProcessabilityGood, + expected: clienttypes.InboundProcessabilityGood, }, { name: "should return InboundProcessabilityComplianceViolation for a restricted sender address", @@ -68,7 +69,7 @@ func Test_CheckProcessability(t *testing.T) { FromAddress: sample.RestrictedBtcAddressTest, ToAddress: testutils.TSSAddressBTCAthens3, }, - expected: observer.InboundProcessabilityComplianceViolation, + expected: clienttypes.InboundProcessabilityComplianceViolation, }, { name: "should return InboundProcessabilityComplianceViolation for a restricted receiver address in standard memo", @@ -81,7 +82,7 @@ func Test_CheckProcessability(t *testing.T) { }, }, }, - expected: observer.InboundProcessabilityComplianceViolation, + expected: clienttypes.InboundProcessabilityComplianceViolation, }, { name: "should return InboundProcessabilityComplianceViolation for a restricted revert address in standard memo", @@ -96,7 +97,7 @@ func Test_CheckProcessability(t *testing.T) { }, }, }, - expected: observer.InboundProcessabilityComplianceViolation, + expected: clienttypes.InboundProcessabilityComplianceViolation, }, { name: "should return InboundProcessabilityDonation for a donation inbound event", @@ -105,7 +106,7 @@ func Test_CheckProcessability(t *testing.T) { ToAddress: testutils.TSSAddressBTCAthens3, MemoBytes: []byte(constant.DonationMessage), }, - expected: observer.InboundProcessabilityDonation, + expected: clienttypes.InboundProcessabilityDonation, }, } diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index aa4f5667ad..65f7b852af 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -361,13 +361,22 @@ func (ob *Observer) GetInboundVoteFromBtcEvent(event *BTCInboundEvent) *crosscha } amountInt := big.NewInt(amountSats) - // create inbound vote message contract V1 for legacy memo + // create inbound vote message contract V1 for legacy memo or standard memo + var msg *crosschaintypes.MsgVoteInbound if event.MemoStd == nil { - return ob.NewInboundVoteFromLegacyMemo(event, amountInt) + msg = ob.NewInboundVoteFromLegacyMemo(event, amountInt) + } else { + msg = ob.NewInboundVoteFromStdMemo(event, amountInt) } - // create inbound vote message for standard memo - return ob.NewInboundVoteFromStdMemo(event, amountInt) + // make sure the message is valid before posting to zetacore + err = msg.ValidateBasic() + if err != nil { + ob.Logger().Inbound.Error().Err(err).Fields(lf).Msg("invalid inbound vote message") + return nil + } + + return msg } // GetBtcEvent returns a valid BTCInboundEvent or nil diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index 7ec938aab0..ba005a1303 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -157,7 +157,9 @@ func Test_GetInboundVoteFromBtcEvent(t *testing.T) { // create test observer ob := MockBTCObserver(t, chain, params, nil) - zetacoreClient := mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}).WithZetaChain() + zetacoreClient := mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{ + OperatorAddress: sample.Bech32AccAddress(), + }).WithZetaChain() ob.WithZetacoreClient(zetacoreClient) // test cases diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index bd0e9a98b7..a560d6b9d0 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -1,7 +1,6 @@ package observer import ( - "bytes" "context" "encoding/hex" "fmt" @@ -13,12 +12,12 @@ import ( "github.com/rs/zerolog" "github.com/zeta-chain/node/pkg/coin" - "github.com/zeta-chain/node/pkg/constant" solanacontracts "github.com/zeta-chain/node/pkg/contracts/solana" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" solanarpc "github.com/zeta-chain/node/zetaclient/chains/solana/rpc" "github.com/zeta-chain/node/zetaclient/compliance" zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/logs" clienttypes "github.com/zeta-chain/node/zetaclient/types" "github.com/zeta-chain/node/zetaclient/zetacore" ) @@ -256,19 +255,29 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* // BuildInboundVoteMsgFromEvent builds a MsgVoteInbound from an inbound event func (ob *Observer) BuildInboundVoteMsgFromEvent(event *clienttypes.InboundEvent) *crosschaintypes.MsgVoteInbound { - // compliance check. Return nil if the inbound contains restricted addresses - if compliance.DoesInboundContainsRestrictedAddress(event, ob.Logger()) { + // prepare logger fields + lf := map[string]any{ + logs.FieldModule: logs.ModNameInbound, + logs.FieldMethod: "BuildInboundVoteMsgFromEvent", + logs.FieldChain: ob.Chain().ChainId, + logs.FieldTx: event.TxHash, + } + + // decode event memo bytes to get the receiver + err := event.DecodeMemo() + if err != nil { + ob.Logger().Inbound.Info().Fields(lf).Msgf("invalid memo bytes: %s", hex.EncodeToString(event.Memo)) return nil } - // donation check - if bytes.Equal(event.Memo, []byte(constant.DonationMessage)) { - ob.Logger().Inbound.Info(). - Msgf("thank you rich folk for your donation! tx %s chain %d", event.TxHash, event.SenderChainID) + // check if the event is processable + if !ob.CheckEventProcessability(*event) { return nil } - return zetacore.GetInboundVoteMessage( + // create inbound vote message + msg := crosschaintypes.NewMsgVoteInbound( + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Sender, event.SenderChainID, event.Sender, @@ -281,7 +290,39 @@ func (ob *Observer) BuildInboundVoteMsgFromEvent(event *clienttypes.InboundEvent 0, event.CoinType, event.Asset, - ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), 0, // not a smart contract call + crosschaintypes.ProtocolContractVersion_V1, + false, // not relevant for v1 ) + + // make sure the message is valid before posting to zetacore + err = msg.ValidateBasic() + if err != nil { + ob.Logger().Inbound.Error().Err(err).Fields(lf).Msg("invalid inbound vote message") + return nil + } + + return msg +} + +// CheckEventProcessability checks if the inbound event is processable +func (ob *Observer) CheckEventProcessability(event clienttypes.InboundEvent) bool { + switch result := event.Processability(); result { + case clienttypes.InboundProcessabilityGood: + return true + case clienttypes.InboundProcessabilityDonation: + logFields := map[string]any{ + logs.FieldChain: ob.Chain().ChainId, + logs.FieldTx: event.TxHash, + } + ob.Logger().Inbound.Info().Fields(logFields).Msgf("thank you rich folk for your donation!") + return false + case clienttypes.InboundProcessabilityComplianceViolation: + compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, + false, ob.Chain().ChainId, event.TxHash, event.Sender, event.Receiver, event.CoinType.String()) + return false + default: + ob.Logger().Inbound.Error().Msgf("unreachable code got InboundProcessability: %v", result) + return false + } } diff --git a/zetaclient/chains/solana/observer/inbound_test.go b/zetaclient/chains/solana/observer/inbound_test.go index 28c31f04db..c8028dc5ce 100644 --- a/zetaclient/chains/solana/observer/inbound_test.go +++ b/zetaclient/chains/solana/observer/inbound_test.go @@ -2,6 +2,7 @@ package observer_test import ( "context" + "strings" "testing" "github.com/stretchr/testify/require" @@ -9,6 +10,7 @@ import ( "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/testutil/sample" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/solana/observer" "github.com/zeta-chain/node/zetaclient/config" @@ -108,7 +110,9 @@ func Test_BuildInboundVoteMsgFromEvent(t *testing.T) { params := sample.ChainParams(chain.ChainId) params.GatewayAddress = sample.SolanaAddress(t) zetacoreClient := mocks.NewZetacoreClient(t) - zetacoreClient.WithKeys(&keys.Keys{}).WithZetaChain().WithPostVoteInbound("", "") + zetacoreClient.WithKeys(&keys.Keys{ + OperatorAddress: sample.Bech32AccAddress(), + }).WithZetaChain().WithPostVoteInbound("", "") database, err := db.NewFromSqliteInMemory(true) require.NoError(t, err) @@ -129,37 +133,85 @@ func Test_BuildInboundVoteMsgFromEvent(t *testing.T) { msg := ob.BuildInboundVoteMsgFromEvent(event) require.NotNil(t, msg) }) - t.Run("should return nil msg if sender is restricted", func(t *testing.T) { - sender := sample.SolanaAddress(t) - receiver := sample.SolanaAddress(t) - event := sample.InboundEvent(chain.ChainId, sender, receiver, 1280, nil) - // restrict sender - cfg.ComplianceConfig.RestrictedAddresses = []string{sender} - config.LoadComplianceConfig(cfg) + t.Run("should return nil if failed to decode memo", func(t *testing.T) { + sender := sample.SolanaAddress(t) + memo := []byte("a memo too short") + event := sample.InboundEvent(chain.ChainId, sender, sender, 1280, memo) msg := ob.BuildInboundVoteMsgFromEvent(event) require.Nil(t, msg) }) - t.Run("should return nil msg if receiver is restricted", func(t *testing.T) { + + t.Run("should return nil if event is not processable", func(t *testing.T) { sender := sample.SolanaAddress(t) receiver := sample.SolanaAddress(t) - memo := sample.EthAddress().Bytes() - event := sample.InboundEvent(chain.ChainId, sender, receiver, 1280, []byte(memo)) + event := sample.InboundEvent(chain.ChainId, sender, receiver, 1280, nil) - // restrict receiver - cfg.ComplianceConfig.RestrictedAddresses = []string{receiver} + // restrict sender + cfg.ComplianceConfig.RestrictedAddresses = []string{sender} config.LoadComplianceConfig(cfg) msg := ob.BuildInboundVoteMsgFromEvent(event) require.Nil(t, msg) }) - t.Run("should return nil msg on donation transaction", func(t *testing.T) { + + t.Run("should return nil if message basic validation fails", func(t *testing.T) { // create event with donation memo sender := sample.SolanaAddress(t) - event := sample.InboundEvent(chain.ChainId, sender, sender, 1280, []byte(constant.DonationMessage)) + maxMsgBytes := crosschaintypes.MaxMessageLength / 2 + event := sample.InboundEvent(chain.ChainId, sender, sender, 1280, []byte(strings.Repeat("a", maxMsgBytes+1))) msg := ob.BuildInboundVoteMsgFromEvent(event) require.Nil(t, msg) }) } + +func Test_CheckEventProcessability(t *testing.T) { + // parepare params + chain := chains.SolanaDevnet + params := sample.ChainParams(chain.ChainId) + params.GatewayAddress = sample.SolanaAddress(t) + + // create test observer + ob := MockSolanaObserver(t, chain, nil, *params, nil, nil) + + // setup compliance config + cfg := config.Config{ + ComplianceConfig: sample.ComplianceConfig(), + } + config.LoadComplianceConfig(cfg) + + // test cases + tests := []struct { + name string + event clienttypes.InboundEvent + result bool + }{ + { + name: "should return true for processable event", + event: clienttypes.InboundEvent{Sender: sample.SolanaAddress(t), Receiver: sample.SolanaAddress(t)}, + result: true, + }, + { + name: "should return false on donation message", + event: clienttypes.InboundEvent{Memo: []byte(constant.DonationMessage)}, + result: false, + }, + { + name: "should return false on compliance violation", + event: clienttypes.InboundEvent{ + Sender: sample.RestrictedSolAddressTest, + Receiver: sample.EthAddress().Hex(), + }, + result: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ob.CheckEventProcessability(tt.event) + require.Equal(t, tt.result, result) + }) + } +} diff --git a/zetaclient/compliance/compliance.go b/zetaclient/compliance/compliance.go index f0135c3ad9..2a16dee6b6 100644 --- a/zetaclient/compliance/compliance.go +++ b/zetaclient/compliance/compliance.go @@ -2,16 +2,10 @@ package compliance import ( - "encoding/hex" - - ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" - "github.com/zeta-chain/node/pkg/memo" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/config" - clienttypes "github.com/zeta-chain/node/zetaclient/types" ) // IsCctxRestricted returns true if the cctx involves restricted addresses @@ -61,21 +55,3 @@ func PrintComplianceLog( inboundLoggerWithFields.Warn().Msg(logMsg) complianceLoggerWithFields.Warn().Msg(logMsg) } - -// DoesInboundContainsRestrictedAddress returns true if the inbound event contains restricted addresses -func DoesInboundContainsRestrictedAddress(event *clienttypes.InboundEvent, logger *base.ObserverLogger) bool { - // parse memo-specified receiver - receiver := "" - parsedAddress, _, err := memo.DecodeLegacyMemoHex(hex.EncodeToString(event.Memo)) - if err == nil && parsedAddress != (ethcommon.Address{}) { - receiver = parsedAddress.Hex() - } - - // check restricted addresses - if config.ContainRestrictedAddress(event.Sender, event.Receiver, receiver) { - PrintComplianceLog(logger.Inbound, logger.Compliance, - false, event.SenderChainID, event.TxHash, event.Sender, receiver, event.CoinType.String()) - return true - } - return false -} diff --git a/zetaclient/types/event.go b/zetaclient/types/event.go index a0313236e6..90bdbbd5ea 100644 --- a/zetaclient/types/event.go +++ b/zetaclient/types/event.go @@ -1,7 +1,31 @@ package types import ( + "bytes" + "encoding/hex" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/pkg/crypto" + "github.com/zeta-chain/node/pkg/memo" + "github.com/zeta-chain/node/zetaclient/config" +) + +// InboundProcessability is an enum representing the processability of an inbound +type InboundProcessability int + +const ( + // InboundProcessabilityGood represents a processable inbound + InboundProcessabilityGood InboundProcessability = iota + + // InboundProcessabilityDonation represents a donation inbound + InboundProcessabilityDonation + + // InboundProcessabilityComplianceViolation represents a compliance violation + InboundProcessabilityComplianceViolation ) // InboundEvent represents an inbound event @@ -41,3 +65,47 @@ type InboundEvent struct { // Asset is the asset of the inbound Asset string } + +// DecodeMemo decodes the receiver from the memo bytes +func (event *InboundEvent) DecodeMemo() error { + // skip decoding donation tx as it won't go through zetacore + if bytes.Equal(event.Memo, []byte(constant.DonationMessage)) { + return nil + } + + // decode receiver address from memo + parsedAddress, _, err := memo.DecodeLegacyMemoHex(hex.EncodeToString(event.Memo)) + if err != nil { // unreachable code + return errors.Wrap(err, "invalid memo hex") + } + + // ensure the receiver is valid + if crypto.IsEmptyAddress(parsedAddress) { + return errors.New("got empty receiver address from memo") + } + event.Receiver = parsedAddress.Hex() + + return nil +} + +// Processability returns the processability of the inbound event +func (event *InboundEvent) Processability() InboundProcessability { + // parse memo-specified receiver + receiver := "" + parsedAddress, _, err := memo.DecodeLegacyMemoHex(hex.EncodeToString(event.Memo)) + if err == nil && parsedAddress != (ethcommon.Address{}) { + receiver = parsedAddress.Hex() + } + + // check restricted addresses + if config.ContainRestrictedAddress(event.Sender, event.Receiver, event.TxOrigin, receiver) { + return InboundProcessabilityComplianceViolation + } + + // donation check + if bytes.Equal(event.Memo, []byte(constant.DonationMessage)) { + return InboundProcessabilityDonation + } + + return InboundProcessabilityGood +} diff --git a/zetaclient/types/event_test.go b/zetaclient/types/event_test.go new file mode 100644 index 0000000000..f8e78b3424 --- /dev/null +++ b/zetaclient/types/event_test.go @@ -0,0 +1,124 @@ +package types_test + +import ( + "testing" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/zetaclient/config" + "github.com/zeta-chain/node/zetaclient/types" +) + +func Test_DecodeMemo(t *testing.T) { + testReceiver := sample.EthAddress() + + // test cases + tests := []struct { + name string + event *types.InboundEvent + expectedReceiver string + errMsg string + }{ + { + name: "should decode receiver address successfully", + event: &types.InboundEvent{ + Memo: testReceiver.Bytes(), + }, + expectedReceiver: testReceiver.Hex(), + }, + { + name: "should skip decoding donation message", + event: &types.InboundEvent{ + Memo: []byte(constant.DonationMessage), + }, + expectedReceiver: "", + }, + { + name: "should return error if got an empty receiver address", + event: &types.InboundEvent{ + Memo: []byte(""), + }, + errMsg: "got empty receiver address from memo", + expectedReceiver: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.event.DecodeMemo() + if tt.errMsg != "" { + require.Contains(t, err.Error(), tt.errMsg) + return + } + require.NoError(t, err) + require.Equal(t, tt.expectedReceiver, tt.event.Receiver) + }) + } +} + +func Test_Processability(t *testing.T) { + // setup compliance config + cfg := config.Config{ + ComplianceConfig: sample.ComplianceConfig(), + } + config.LoadComplianceConfig(cfg) + + // test cases + tests := []struct { + name string + event *types.InboundEvent + expected types.InboundProcessability + }{ + { + name: "should return InboundProcessabilityGood for a processable inbound event", + event: &types.InboundEvent{ + Sender: sample.SolanaAddress(t), + Receiver: sample.EthAddress().Hex(), + }, + expected: types.InboundProcessabilityGood, + }, + { + name: "should return InboundProcessabilityComplianceViolation for a restricted sender address", + event: &types.InboundEvent{ + Sender: sample.RestrictedSolAddressTest, + Receiver: sample.EthAddress().Hex(), + }, + expected: types.InboundProcessabilityComplianceViolation, + }, + { + name: "should return InboundProcessabilityComplianceViolation for a restricted receiver address", + event: &types.InboundEvent{ + Sender: sample.SolanaAddress(t), + Receiver: sample.RestrictedSolAddressTest, + }, + expected: types.InboundProcessabilityComplianceViolation, + }, + { + name: "should return InboundProcessabilityComplianceViolation for a restricted receiver address in memo", + event: &types.InboundEvent{ + Sender: sample.SolanaAddress(t), + Receiver: sample.EthAddress().Hex(), + Memo: ethcommon.HexToAddress(sample.RestrictedEVMAddressTest).Bytes(), + }, + expected: types.InboundProcessabilityComplianceViolation, + }, + { + name: "should return InboundProcessabilityDonation for a donation inbound event", + event: &types.InboundEvent{ + Sender: sample.SolanaAddress(t), + Receiver: sample.EthAddress().Hex(), + Memo: []byte(constant.DonationMessage), + }, + expected: types.InboundProcessabilityDonation, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.event.Processability() + require.Equal(t, tt.expected, result) + }) + } +}