diff --git a/Makefile b/Makefile index 79eea798..91a217ae 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,7 @@ genmocks: mockgen -source=chains/evm/transactor/transact.go -destination=./mock/transact.go -package mock mockgen -source=chains/evm/transactor/signAndSend/signAndSend.go -destination=./mock/signAndSend.go -package mock mockgen -source=./store/store.go -destination=./mock/store.go -package mock + mockgen -destination=chains/evm/eventhandlers/mock/eventhandlers.go -source=./chains/evm/eventhandlers/event-handler.go -package mock mockgen -source=./relayer/message/handler.go -destination=./mock/message.go -package mock mockgen -source=./chains/evm/listener/listener.go -destination=./mock/evmListener.go -package mock mockgen -destination=./mock/substrateListener.go -package mock github.com/sygmaprotocol/sygma-core/chains/substrate/listener ChainConnection diff --git a/chains/evm/deposithandlers/deposit-handler.go b/chains/evm/deposithandlers/deposit-handler.go new file mode 100644 index 00000000..83d43c6e --- /dev/null +++ b/chains/evm/deposithandlers/deposit-handler.go @@ -0,0 +1,64 @@ +package deposithandlers + +import ( + "errors" + + "github.com/ChainSafe/sygma-core/chains/evm/eventhandlers" + + "github.com/ChainSafe/sygma-core/types" + "github.com/rs/zerolog/log" + + "github.com/ethereum/go-ethereum/common" +) + +type DepositHandlers map[common.Address]eventhandlers.DepositHandler +type HandlerMatcher interface { + GetHandlerAddressForResourceID(resourceID types.ResourceID) (common.Address, error) +} + +type ETHDepositHandler struct { + handlerMatcher HandlerMatcher + depositHandlers DepositHandlers +} + +// NewETHDepositHandler creates an instance of ETHDepositHandler that contains +// handler functions for processing deposit events +func NewETHDepositHandler(handlerMatcher HandlerMatcher) *ETHDepositHandler { + return ÐDepositHandler{ + handlerMatcher: handlerMatcher, + depositHandlers: make(map[common.Address]eventhandlers.DepositHandler), + } +} + +func (e *ETHDepositHandler) HandleDeposit(sourceID, destID uint8, depositNonce uint64, resourceID types.ResourceID, calldata, handlerResponse []byte) (*types.Message, error) { + handlerAddr, err := e.handlerMatcher.GetHandlerAddressForResourceID(resourceID) + if err != nil { + return nil, err + } + + depositHandler, err := e.matchAddressWithHandlerFunc(handlerAddr) + if err != nil { + return nil, err + } + + return depositHandler.HandleDeposit(sourceID, destID, depositNonce, resourceID, calldata, handlerResponse) +} + +// matchAddressWithHandlerFunc matches a handler address with an associated handler function +func (e *ETHDepositHandler) matchAddressWithHandlerFunc(handlerAddress common.Address) (eventhandlers.DepositHandler, error) { + hf, ok := e.depositHandlers[handlerAddress] + if !ok { + return nil, errors.New("no corresponding deposit handler for this address exists") + } + return hf, nil +} + +// RegisterDepositHandler registers an event handler by associating a handler function to a specified address +func (e *ETHDepositHandler) RegisterDepositHandler(handlerAddress string, handler eventhandlers.DepositHandler) { + if handlerAddress == "" { + return + } + + log.Debug().Msgf("Registered deposit handler for address %s", handlerAddress) + e.depositHandlers[common.HexToAddress(handlerAddress)] = handler +} diff --git a/chains/evm/deposithandlers/erc20.go b/chains/evm/deposithandlers/erc20.go new file mode 100644 index 00000000..217bcefd --- /dev/null +++ b/chains/evm/deposithandlers/erc20.go @@ -0,0 +1,54 @@ +package deposithandlers + +import ( + "errors" + "math/big" + + "github.com/ChainSafe/sygma-core/types" +) + +type Erc20DepositHandler struct{} + +// Erc20DepositHandler converts data pulled from event logs into message +// handlerResponse can be an empty slice +func (dh *Erc20DepositHandler) HandleDeposit(sourceID, destId uint8, nonce uint64, resourceID types.ResourceID, calldata, handlerResponse []byte) (*types.Message, error) { + if len(calldata) < 84 { + err := errors.New("invalid calldata length: less than 84 bytes") + return nil, err + } + + // @dev + // amount: first 32 bytes of calldata + amount := calldata[:32] + + // lenRecipientAddress: second 32 bytes of calldata [32:64] + // does not need to be derived because it is being calculated + // within ERC20MessageHandler + // https://github.com/ChainSafe/chainbridge-core/blob/main/chains/evm/voter/message-handler.go#L108 + + // 32-64 is recipient address length + recipientAddressLength := big.NewInt(0).SetBytes(calldata[32:64]) + + // 64 - (64 + recipient address length) is recipient address + recipientAddress := calldata[64:(64 + recipientAddressLength.Int64())] + + // if there is priority data, parse it and use it + payload := []interface{}{ + amount, + recipientAddress, + } + + // arbitrary metadata that will be most likely be used by the relayer + var metadata types.Metadata + if 64+recipientAddressLength.Int64() < int64(len(calldata)) { + priorityLength := big.NewInt(0).SetBytes(calldata[(64 + recipientAddressLength.Int64()):((64 + recipientAddressLength.Int64()) + 1)]) + + // (64 + recipient address length + 1) - ((64 + recipient address length + 1) + priority length) is priority data + priority := calldata[(64 + recipientAddressLength.Int64() + 1):((64 + recipientAddressLength.Int64()) + 1 + priorityLength.Int64())] + + // Assign the priority data to the Metadata struct + metadata.Data = make(map[string]interface{}) + metadata.Data["Priority"] = priority[0] + } + return types.NewMessage(sourceID, destId, nonce, resourceID, types.FungibleTransfer, payload, metadata), nil +} diff --git a/chains/evm/deposithandlers/erc20_test.go b/chains/evm/deposithandlers/erc20_test.go new file mode 100644 index 00000000..e9539a8b --- /dev/null +++ b/chains/evm/deposithandlers/erc20_test.go @@ -0,0 +1,138 @@ +package deposithandlers_test + +import ( + "errors" + "math/big" + "testing" + + "github.com/ChainSafe/chainbridge-core/chains/evm/calls/contracts/deposit" + "github.com/ChainSafe/sygma-core/chains/evm/deposithandlers" + "github.com/ChainSafe/sygma-core/chains/evm/eventhandlers" + "github.com/ChainSafe/sygma-core/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/stretchr/testify/suite" +) + +var errIncorrectDataLen = errors.New("invalid calldata length: less than 84 bytes") + +type Erc20HandlerTestSuite struct { + suite.Suite +} + +func TestRunErc20HandlerTestSuite(t *testing.T) { + suite.Run(t, new(Erc20HandlerTestSuite)) +} + +func (s *Erc20HandlerTestSuite) SetupSuite() {} +func (s *Erc20HandlerTestSuite) TearDownSuite() {} +func (s *Erc20HandlerTestSuite) SetupTest() {} +func (s *Erc20HandlerTestSuite) TearDownTest() {} + +func (s *Erc20HandlerTestSuite) TestErc20HandleEvent() { + // 0xf1e58fb17704c2da8479a533f9fad4ad0993ca6b + recipientByteSlice := []byte{241, 229, 143, 177, 119, 4, 194, 218, 132, 121, 165, 51, 249, 250, 212, 173, 9, 147, 202, 107} + + calldata := deposit.ConstructErc20DepositData(recipientByteSlice, big.NewInt(2)) + depositLog := &eventhandlers.Deposit{ + DestinationDomainID: 0, + ResourceID: [32]byte{0}, + DepositNonce: 1, + SenderAddress: common.HexToAddress("0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485"), + Data: calldata, + HandlerResponse: []byte{}, + } + + sourceID := uint8(1) + amountParsed := calldata[:32] + recipientAddressParsed := calldata[64:] + + expected := &types.Message{ + Source: sourceID, + Destination: depositLog.DestinationDomainID, + DepositNonce: depositLog.DepositNonce, + ResourceId: depositLog.ResourceID, + Type: types.FungibleTransfer, + Payload: []interface{}{ + amountParsed, + recipientAddressParsed, + }, + } + erc20DepositHandler := deposithandlers.Erc20DepositHandler{} + message, err := erc20DepositHandler.HandleDeposit(sourceID, depositLog.DestinationDomainID, depositLog.DepositNonce, depositLog.ResourceID, depositLog.Data, depositLog.HandlerResponse) + + s.Nil(err) + s.NotNil(message) + s.Equal(message, expected) +} + +func (s *Erc20HandlerTestSuite) TestErc20HandleEventWithPriority() { + // 0xf1e58fb17704c2da8479a533f9fad4ad0993ca6b + recipientByteSlice := []byte{241, 229, 143, 177, 119, 4, 194, 218, 132, 121, 165, 51, 249, 250, 212, 173, 9, 147, 202, 107} + + calldata := deposit.ConstructErc20DepositDataWithPriority(recipientByteSlice, big.NewInt(2), uint8(1)) + depositLog := &eventhandlers.Deposit{ + DestinationDomainID: 0, + ResourceID: [32]byte{0}, + DepositNonce: 1, + SenderAddress: common.HexToAddress("0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485"), + Data: calldata, + HandlerResponse: []byte{}, + } + + sourceID := uint8(1) + amountParsed := calldata[:32] + // 32-64 is recipient address length + recipientAddressLength := big.NewInt(0).SetBytes(calldata[32:64]) + + // 64 - (64 + recipient address length) is recipient address + recipientAddressParsed := calldata[64:(64 + recipientAddressLength.Int64())] + expected := &types.Message{ + Source: sourceID, + Destination: depositLog.DestinationDomainID, + DepositNonce: depositLog.DepositNonce, + ResourceId: depositLog.ResourceID, + Type: types.FungibleTransfer, + Payload: []interface{}{ + amountParsed, + recipientAddressParsed, + }, + Metadata: types.Metadata{ + Data: map[string]interface{}{ + "Priority": uint8(1), + }, + }, + } + + erc20DepositHandler := deposithandlers.Erc20DepositHandler{} + message, err := erc20DepositHandler.HandleDeposit(sourceID, depositLog.DestinationDomainID, depositLog.DepositNonce, depositLog.ResourceID, depositLog.Data, depositLog.HandlerResponse) + + s.Nil(err) + s.NotNil(message) + s.Equal(message, expected) +} + +func (s *Erc20HandlerTestSuite) TestErc20HandleEventIncorrectDataLen() { + metadata := []byte("0xdeadbeef") + + var calldata []byte + calldata = append(calldata, math.PaddedBigBytes(big.NewInt(int64(len(metadata))), 32)...) + calldata = append(calldata, metadata...) + + depositLog := &eventhandlers.Deposit{ + DestinationDomainID: 0, + ResourceID: [32]byte{0}, + DepositNonce: 1, + SenderAddress: common.HexToAddress("0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485"), + Data: calldata, + HandlerResponse: []byte{}, + } + + sourceID := uint8(1) + + erc20DepositHandler := deposithandlers.Erc20DepositHandler{} + message, err := erc20DepositHandler.HandleDeposit(sourceID, depositLog.DestinationDomainID, depositLog.DepositNonce, depositLog.ResourceID, depositLog.Data, depositLog.HandlerResponse) + + s.Nil(message) + s.EqualError(err, errIncorrectDataLen.Error()) +} diff --git a/chains/evm/deposithandlers/erc721.go b/chains/evm/deposithandlers/erc721.go new file mode 100644 index 00000000..d40c3bbe --- /dev/null +++ b/chains/evm/deposithandlers/erc721.go @@ -0,0 +1,59 @@ +package deposithandlers + +import ( + "errors" + "math/big" + + "github.com/ChainSafe/sygma-core/types" +) + +type Erc721DepositHandler struct{} + +// Erc721DepositHandler converts data pulled from ERC721 deposit event logs into message +func (dh *Erc721DepositHandler) HandleDeposit(sourceID, destId uint8, nonce uint64, resourceID types.ResourceID, calldata, handlerResponse []byte) (*types.Message, error) { + if len(calldata) < 64 { + err := errors.New("invalid calldata length: less than 84 bytes") + return nil, err + } + + // first 32 bytes are tokenId + tokenId := calldata[:32] + + // 32 - 64 is recipient address length + recipientAddressLength := big.NewInt(0).SetBytes(calldata[32:64]) + + // 64 - (64 + recipient address length) is recipient address + recipientAddress := calldata[64:(64 + recipientAddressLength.Int64())] + + // (64 + recipient address length) - ((64 + recipient address length) + 32) is metadata length + metadataLength := big.NewInt(0).SetBytes( + calldata[(64 + recipientAddressLength.Int64()):((64 + recipientAddressLength.Int64()) + 32)], + ) + // ((64 + recipient address length) + 32) - ((64 + recipient address length) + 32 + metadata length) is metadata + var metadata []byte + var metadataStart int64 + if metadataLength.Cmp(big.NewInt(0)) == 1 { + metadataStart = (64 + recipientAddressLength.Int64()) + 32 + metadata = calldata[metadataStart : metadataStart+metadataLength.Int64()] + } + // arbitrary metadata that will be most likely be used by the relayer + var meta types.Metadata + + payload := []interface{}{ + tokenId, + recipientAddress, + metadata, + } + + if 64+recipientAddressLength.Int64()+32+metadataLength.Int64() < int64(len(calldata)) { + // (metadataStart + metadataLength) - (metadataStart + metadataLength + 1) is priority length + priorityLength := big.NewInt(0).SetBytes(calldata[(64 + recipientAddressLength.Int64() + 32 + metadataLength.Int64()):(64 + recipientAddressLength.Int64() + 32 + metadataLength.Int64() + 1)]) + // (metadataStart + metadataLength + 1) - (metadataStart + metadataLength + 1) + priority length) is priority data + priority := calldata[(64 + recipientAddressLength.Int64() + 32 + metadataLength.Int64() + 1):(64 + recipientAddressLength.Int64() + 32 + metadataLength.Int64() + 1 + priorityLength.Int64())] + + // Assign the priority data to the Metadata struct + meta.Data = make(map[string]interface{}) + meta.Data["Priority"] = priority[0] + } + return types.NewMessage(sourceID, destId, nonce, resourceID, types.NonFungibleTransfer, payload, meta), nil +} diff --git a/chains/evm/deposithandlers/erc721_test.go b/chains/evm/deposithandlers/erc721_test.go new file mode 100644 index 00000000..b730742b --- /dev/null +++ b/chains/evm/deposithandlers/erc721_test.go @@ -0,0 +1,181 @@ +package deposithandlers_test + +import ( + "math/big" + "testing" + + "github.com/ChainSafe/chainbridge-core/chains/evm/calls/contracts/deposit" + "github.com/ChainSafe/sygma-core/chains/evm/deposithandlers" + "github.com/ChainSafe/sygma-core/chains/evm/eventhandlers" + "github.com/ChainSafe/sygma-core/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/stretchr/testify/suite" +) + +type Erc721HandlerTestSuite struct { + suite.Suite +} + +func TestRunErc721HandlerTestSuite(t *testing.T) { + suite.Run(t, new(Erc721HandlerTestSuite)) +} + +func (s *Erc721HandlerTestSuite) SetupSuite() {} +func (s *Erc721HandlerTestSuite) TearDownSuite() {} +func (s *Erc721HandlerTestSuite) SetupTest() {} +func (s *Erc721HandlerTestSuite) TearDownTest() {} + +func (s *Erc721HandlerTestSuite) TestErc721DepositHandlerEmptyMetadata() { + recipient := common.HexToAddress("0xf1e58fb17704c2da8479a533f9fad4ad0993ca6b") + + calldata := deposit.ConstructErc721DepositData(recipient.Bytes(), big.NewInt(2), []byte{}) + depositLog := &eventhandlers.Deposit{ + DestinationDomainID: 0, + ResourceID: [32]byte{0}, + DepositNonce: 1, + Data: calldata, + HandlerResponse: []byte{}, + } + + sourceID := uint8(1) + tokenId := calldata[:32] + recipientAddressParsed := calldata[64:84] + var metadata []byte + + expected := &types.Message{ + Source: sourceID, + Destination: depositLog.DestinationDomainID, + DepositNonce: depositLog.DepositNonce, + ResourceId: depositLog.ResourceID, + Type: types.NonFungibleTransfer, + Payload: []interface{}{ + tokenId, + recipientAddressParsed, + metadata, + }, + } + + erc721DepositHandler := deposithandlers.Erc721DepositHandler{} + m, err := erc721DepositHandler.HandleDeposit(sourceID, depositLog.DestinationDomainID, depositLog.DepositNonce, depositLog.ResourceID, depositLog.Data, depositLog.HandlerResponse) + + s.Nil(err) + s.NotNil(m) + s.Equal(expected, m) +} + +func (s *Erc721HandlerTestSuite) TestErc721DepositHandlerIncorrectDataLen() { + metadata := []byte("0xdeadbeef") + + var calldata []byte + calldata = append(calldata, math.PaddedBigBytes(big.NewInt(int64(len(metadata))), 16)...) + calldata = append(calldata, metadata...) + + depositLog := &eventhandlers.Deposit{ + DestinationDomainID: 0, + ResourceID: [32]byte{0}, + DepositNonce: 1, + SenderAddress: common.HexToAddress("0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485"), + Data: calldata, + HandlerResponse: []byte{}, + } + + sourceID := uint8(1) + + erc721DepositHandler := deposithandlers.Erc721DepositHandler{} + m, err := erc721DepositHandler.HandleDeposit(sourceID, depositLog.DestinationDomainID, depositLog.DepositNonce, depositLog.ResourceID, depositLog.Data, depositLog.HandlerResponse) + s.Nil(m) + s.EqualError(err, "invalid calldata length: less than 84 bytes") +} + +func (s *Erc721HandlerTestSuite) TestErc721DepositHandler() { + recipient := common.HexToAddress("0xf1e58fb17704c2da8479a533f9fad4ad0993ca6b") + metadata := []byte("metadata.url") + + calldata := deposit.ConstructErc721DepositData(recipient.Bytes(), big.NewInt(2), metadata) + depositLog := &eventhandlers.Deposit{ + DestinationDomainID: 0, + ResourceID: [32]byte{0}, + DepositNonce: 1, + Data: calldata, + HandlerResponse: []byte{}, + } + + sourceID := uint8(1) + tokenId := calldata[:32] + recipientAddressParsed := calldata[64:84] + parsedMetadata := calldata[116:128] + + expected := &types.Message{ + Source: sourceID, + Destination: depositLog.DestinationDomainID, + DepositNonce: depositLog.DepositNonce, + ResourceId: depositLog.ResourceID, + Type: types.NonFungibleTransfer, + Payload: []interface{}{ + tokenId, + recipientAddressParsed, + parsedMetadata, + }, + } + + erc721DepositHandler := deposithandlers.Erc721DepositHandler{} + m, err := erc721DepositHandler.HandleDeposit(sourceID, depositLog.DestinationDomainID, depositLog.DepositNonce, depositLog.ResourceID, depositLog.Data, depositLog.HandlerResponse) + s.Nil(err) + s.NotNil(m) + s.Equal(expected, m) +} +func (s *Erc721HandlerTestSuite) TestErc721DepositHandlerWithPriority() { + recipient := common.HexToAddress("0xf1e58fb17704c2da8479a533f9fad4ad0993ca6b") + metadata := []byte("metadata.url") + + calldata := deposit.ConstructErc721DepositDataWithPriority(recipient.Bytes(), big.NewInt(2), metadata, uint8(1)) + depositLog := &eventhandlers.Deposit{ + DestinationDomainID: 0, + ResourceID: [32]byte{0}, + DepositNonce: 1, + Data: calldata, + HandlerResponse: []byte{}, + } + + sourceID := uint8(1) + tokenId := calldata[:32] + + // 32 - 64 is recipient address length + recipientAddressLength := big.NewInt(0).SetBytes(calldata[32:64]) + + // 64 - (64 + recipient address length) is recipient address + recipientAddressParsed := calldata[64:(64 + recipientAddressLength.Int64())] + + // (64 + recipient address length) - ((64 + recipient address length) + 32) is metadata length + metadataLength := big.NewInt(0).SetBytes( + calldata[(64 + recipientAddressLength.Int64()):((64 + recipientAddressLength.Int64()) + 32)], + ) + + metadataStart := (64 + recipientAddressLength.Int64()) + 32 + parsedMetadata := calldata[metadataStart : metadataStart+metadataLength.Int64()] + + expected := &types.Message{ + Source: sourceID, + Destination: depositLog.DestinationDomainID, + DepositNonce: depositLog.DepositNonce, + ResourceId: depositLog.ResourceID, + Type: types.NonFungibleTransfer, + Payload: []interface{}{ + tokenId, + recipientAddressParsed, + parsedMetadata, + }, + Metadata: types.Metadata{ + Data: map[string]interface{}{ + "Priority": uint8(1), + }, + }, + } + + erc721DepositHandler := deposithandlers.Erc721DepositHandler{} + m, err := erc721DepositHandler.HandleDeposit(sourceID, depositLog.DestinationDomainID, depositLog.DepositNonce, depositLog.ResourceID, depositLog.Data, depositLog.HandlerResponse) + s.Nil(err) + s.NotNil(m) + s.Equal(expected, m) +} diff --git a/chains/evm/deposithandlers/generic.go b/chains/evm/deposithandlers/generic.go new file mode 100644 index 00000000..2b2fe4ce --- /dev/null +++ b/chains/evm/deposithandlers/generic.go @@ -0,0 +1,29 @@ +package deposithandlers + +import ( + "errors" + "math/big" + + "github.com/ChainSafe/sygma-core/types" +) + +type GenericDepositHandler struct{} + +// GenericDepositHandler converts data pulled from generic deposit event logs into message +func (dh *GenericDepositHandler) HandleDeposit(sourceID, destId uint8, nonce uint64, resourceID types.ResourceID, calldata, handlerResponse []byte) (*types.Message, error) { + if len(calldata) < 32 { + err := errors.New("invalid calldata length: less than 32 bytes") + return nil, err + } + + // first 32 bytes are metadata length + metadataLen := big.NewInt(0).SetBytes(calldata[:32]) + metadata := calldata[32 : 32+metadataLen.Int64()] + payload := []interface{}{ + metadata, + } + + // generic handler has specific payload length and doesn't support arbitrary metadata + meta := types.Metadata{} + return types.NewMessage(sourceID, destId, nonce, resourceID, types.GenericTransfer, payload, meta), nil +} diff --git a/chains/evm/deposithandlers/generic_test.go b/chains/evm/deposithandlers/generic_test.go new file mode 100644 index 00000000..6df75b0f --- /dev/null +++ b/chains/evm/deposithandlers/generic_test.go @@ -0,0 +1,139 @@ +package deposithandlers_test + +import ( + "math/big" + "testing" + + "github.com/ChainSafe/chainbridge-core/chains/evm/calls/contracts/deposit" + "github.com/ChainSafe/sygma-core/chains/evm/deposithandlers" + "github.com/ChainSafe/sygma-core/chains/evm/eventhandlers" + "github.com/ChainSafe/sygma-core/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/stretchr/testify/suite" +) + +type GenericHandlerTestSuite struct { + suite.Suite +} + +func TestRunGenericHandlerTestSuite(t *testing.T) { + suite.Run(t, new(GenericHandlerTestSuite)) +} + +func (s *GenericHandlerTestSuite) SetupSuite() {} +func (s *GenericHandlerTestSuite) TearDownSuite() {} +func (s *GenericHandlerTestSuite) SetupTest() {} +func (s *GenericHandlerTestSuite) TearDownTest() {} + +func (s *GenericHandlerTestSuite) TestGenericHandleEventIncorrectDataLen() { + metadata := []byte("0xdeadbeef") + + var calldata []byte + calldata = append(calldata, math.PaddedBigBytes(big.NewInt(int64(len(metadata))), 16)...) + calldata = append(calldata, metadata...) + + depositLog := &eventhandlers.Deposit{ + DestinationDomainID: 0, + ResourceID: [32]byte{0}, + DepositNonce: 1, + SenderAddress: common.HexToAddress("0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485"), + Data: calldata, + HandlerResponse: []byte{}, + } + + sourceID := uint8(1) + + genericDepositHandler := deposithandlers.GenericDepositHandler{} + message, err := genericDepositHandler.HandleDeposit( + sourceID, + depositLog.DestinationDomainID, + depositLog.DepositNonce, + depositLog.ResourceID, + depositLog.Data, + depositLog.HandlerResponse, + ) + + s.Nil(message) + s.EqualError(err, "invalid calldata length: less than 32 bytes") +} + +func (s *GenericHandlerTestSuite) TestGenericHandleEventEmptyMetadata() { + metadata := []byte("") + calldata := deposit.ConstructGenericDepositData(metadata) + + depositLog := &eventhandlers.Deposit{ + DestinationDomainID: 0, + ResourceID: [32]byte{0}, + DepositNonce: 1, + SenderAddress: common.HexToAddress("0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485"), + Data: calldata, + HandlerResponse: []byte{}, + } + + sourceID := uint8(1) + expected := &types.Message{ + Source: sourceID, + Destination: depositLog.DestinationDomainID, + DepositNonce: depositLog.DepositNonce, + ResourceId: depositLog.ResourceID, + Type: types.GenericTransfer, + Payload: []interface{}{ + metadata, + }, + } + + genericDepositHandler := deposithandlers.GenericDepositHandler{} + message, err := genericDepositHandler.HandleDeposit( + sourceID, + depositLog.DestinationDomainID, + depositLog.DepositNonce, + depositLog.ResourceID, + depositLog.Data, + depositLog.HandlerResponse, + ) + + s.Nil(err) + s.NotNil(message) + s.Equal(message, expected) +} + +func (s *GenericHandlerTestSuite) TestGenericHandleEvent() { + metadata := []byte("0xdeadbeef") + calldata := deposit.ConstructGenericDepositData(metadata) + + depositLog := &eventhandlers.Deposit{ + DestinationDomainID: 0, + ResourceID: [32]byte{0}, + DepositNonce: 1, + SenderAddress: common.HexToAddress("0x4CEEf6139f00F9F4535Ad19640Ff7A0137708485"), + Data: calldata, + HandlerResponse: []byte{}, + } + + sourceID := uint8(1) + expected := &types.Message{ + Source: sourceID, + Destination: depositLog.DestinationDomainID, + DepositNonce: depositLog.DepositNonce, + ResourceId: depositLog.ResourceID, + Type: types.GenericTransfer, + Payload: []interface{}{ + metadata, + }, + } + + genericDepositHandler := deposithandlers.GenericDepositHandler{} + message, err := genericDepositHandler.HandleDeposit( + sourceID, + depositLog.DestinationDomainID, + depositLog.DepositNonce, + depositLog.ResourceID, + depositLog.Data, + depositLog.HandlerResponse, + ) + + s.Nil(err) + s.NotNil(message) + s.Equal(message, expected) +} diff --git a/chains/evm/eventhandlers/event-handler.go b/chains/evm/eventhandlers/event-handler.go new file mode 100644 index 00000000..5e3e0dca --- /dev/null +++ b/chains/evm/eventhandlers/event-handler.go @@ -0,0 +1,90 @@ +package eventhandlers + +import ( + "context" + "fmt" + "math/big" + + "github.com/ChainSafe/sygma-core/types" + "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog/log" +) + +// Deposit struct holds event data with all necessary parameters and a handler response +// https://github.com/ChainSafe/chainbridge-solidity/blob/develop/contracts/Bridge.sol#L47 +type Deposit struct { + // ID of chain deposit will be bridged to + DestinationDomainID uint8 + // ResourceID used to find address of handler to be used for deposit + ResourceID types.ResourceID + // Nonce of deposit + DepositNonce uint64 + // Address of sender (msg.sender: user) + SenderAddress common.Address + // Additional data to be passed to specified handler + Data []byte + // ERC20Handler: responds with empty data + // ERC721Handler: responds with deposited token metadata acquired by calling a tokenURI method in the token contract + // GenericHandler: responds with the raw bytes returned from the call to the target contract + HandlerResponse []byte +} + +type EventListener interface { + FetchDeposits(ctx context.Context, address common.Address, startBlock *big.Int, endBlock *big.Int) ([]*Deposit, error) +} + +type DepositHandler interface { + HandleDeposit(sourceID, destID uint8, nonce uint64, resourceID types.ResourceID, calldata, handlerResponse []byte) (*types.Message, error) +} + +type DepositEventHandler struct { + eventListener EventListener + depositHandler DepositHandler + + bridgeAddress common.Address + domainID uint8 +} + +func NewDepositEventHandler(eventListener EventListener, depositHandler DepositHandler, bridgeAddress common.Address, domainID uint8) *DepositEventHandler { + return &DepositEventHandler{ + eventListener: eventListener, + depositHandler: depositHandler, + bridgeAddress: bridgeAddress, + domainID: domainID, + } +} + +func (eh *DepositEventHandler) HandleEvent(startBlock *big.Int, endBlock *big.Int, msgChan chan []*types.Message) error { + deposits, err := eh.eventListener.FetchDeposits(context.Background(), eh.bridgeAddress, startBlock, endBlock) + if err != nil { + return fmt.Errorf("unable to fetch deposit events because of: %+v", err) + } + + domainDeposits := make(map[uint8][]*types.Message) + for _, d := range deposits { + func(d *Deposit) { + defer func() { + if r := recover(); r != nil { + log.Error().Err(err).Msgf("panic occured while handling deposit %+v", d) + } + }() + + m, err := eh.depositHandler.HandleDeposit(eh.domainID, d.DestinationDomainID, d.DepositNonce, d.ResourceID, d.Data, d.HandlerResponse) + if err != nil { + log.Error().Err(err).Str("start block", startBlock.String()).Str("end block", endBlock.String()).Uint8("domainID", eh.domainID).Msgf("%v", err) + return + } + + log.Debug().Msgf("Resolved message %+v in block range: %s-%s", m, startBlock.String(), endBlock.String()) + domainDeposits[m.Destination] = append(domainDeposits[m.Destination], m) + }(d) + } + + for _, deposits := range domainDeposits { + go func(d []*types.Message) { + msgChan <- d + }(deposits) + } + + return nil +} diff --git a/chains/evm/eventhandlers/event-handler_test.go b/chains/evm/eventhandlers/event-handler_test.go new file mode 100644 index 00000000..60023c6f --- /dev/null +++ b/chains/evm/eventhandlers/event-handler_test.go @@ -0,0 +1,184 @@ +package eventhandlers_test + +import ( + "fmt" + "math/big" + "testing" + + "github.com/ChainSafe/sygma-core/chains/evm/eventhandlers" + mock_listener "github.com/ChainSafe/sygma-core/chains/evm/eventhandlers/mock" + "github.com/ChainSafe/sygma-core/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" +) + +type DepositHandlerTestSuite struct { + suite.Suite + depositEventHandler *eventhandlers.DepositEventHandler + mockDepositHandler *mock_listener.MockDepositHandler + mockEventListener *mock_listener.MockEventListener + domainID uint8 +} + +func TestRunDepositHandlerTestSuite(t *testing.T) { + suite.Run(t, new(DepositHandlerTestSuite)) +} + +func (s *DepositHandlerTestSuite) SetupTest() { + ctrl := gomock.NewController(s.T()) + s.domainID = 1 + s.mockEventListener = mock_listener.NewMockEventListener(ctrl) + s.mockDepositHandler = mock_listener.NewMockDepositHandler(ctrl) + s.depositEventHandler = eventhandlers.NewDepositEventHandler(s.mockEventListener, s.mockDepositHandler, common.Address{}, s.domainID) +} + +func (s *DepositHandlerTestSuite) Test_FetchDepositFails() { + s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]*eventhandlers.Deposit{}, fmt.Errorf("error")) + + msgChan := make(chan []*types.Message, 1) + err := s.depositEventHandler.HandleEvent(big.NewInt(0), big.NewInt(5), msgChan) + + s.NotNil(err) + s.Equal(len(msgChan), 0) +} + +func (s *DepositHandlerTestSuite) Test_HandleDepositFails_ExecutionContinue() { + d1 := &eventhandlers.Deposit{ + DepositNonce: 1, + DestinationDomainID: 2, + ResourceID: types.ResourceID{}, + HandlerResponse: []byte{}, + Data: []byte{}, + } + d2 := &eventhandlers.Deposit{ + DepositNonce: 2, + DestinationDomainID: 2, + ResourceID: types.ResourceID{}, + HandlerResponse: []byte{}, + Data: []byte{}, + } + deposits := []*eventhandlers.Deposit{d1, d2} + s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(deposits, nil) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d1.DestinationDomainID, + d1.DepositNonce, + d1.ResourceID, + d1.Data, + d1.HandlerResponse, + ).Return(&types.Message{}, fmt.Errorf("error")) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d2.DestinationDomainID, + d2.DepositNonce, + d2.ResourceID, + d2.Data, + d2.HandlerResponse, + ).Return( + &types.Message{DepositNonce: 2}, + nil, + ) + + msgChan := make(chan []*types.Message, 2) + err := s.depositEventHandler.HandleEvent(big.NewInt(0), big.NewInt(5), msgChan) + msgs := <-msgChan + + s.Nil(err) + s.Equal(msgs, []*types.Message{{DepositNonce: 2}}) +} + +func (s *DepositHandlerTestSuite) Test_HandleDepositPanis_ExecutionContinues() { + d1 := &eventhandlers.Deposit{ + DepositNonce: 1, + DestinationDomainID: 2, + ResourceID: types.ResourceID{}, + HandlerResponse: []byte{}, + Data: []byte{}, + } + d2 := &eventhandlers.Deposit{ + DepositNonce: 2, + DestinationDomainID: 2, + ResourceID: types.ResourceID{}, + HandlerResponse: []byte{}, + Data: []byte{}, + } + deposits := []*eventhandlers.Deposit{d1, d2} + s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(deposits, nil) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d1.DestinationDomainID, + d1.DepositNonce, + d1.ResourceID, + d1.Data, + d1.HandlerResponse, + ).Do(func(sourceID, destID, nonce, resourceID, calldata, handlerResponse interface{}) { + panic("error") + }) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d2.DestinationDomainID, + d2.DepositNonce, + d2.ResourceID, + d2.Data, + d2.HandlerResponse, + ).Return( + &types.Message{DepositNonce: 2}, + nil, + ) + + msgChan := make(chan []*types.Message, 2) + err := s.depositEventHandler.HandleEvent(big.NewInt(0), big.NewInt(5), msgChan) + msgs := <-msgChan + + s.Nil(err) + s.Equal(msgs, []*types.Message{{DepositNonce: 2}}) +} + +func (s *DepositHandlerTestSuite) Test_SuccessfulHandleDeposit() { + d1 := &eventhandlers.Deposit{ + DepositNonce: 1, + DestinationDomainID: 2, + ResourceID: types.ResourceID{}, + HandlerResponse: []byte{}, + Data: []byte{}, + } + d2 := &eventhandlers.Deposit{ + DepositNonce: 2, + DestinationDomainID: 2, + ResourceID: types.ResourceID{}, + HandlerResponse: []byte{}, + Data: []byte{}, + } + deposits := []*eventhandlers.Deposit{d1, d2} + s.mockEventListener.EXPECT().FetchDeposits(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(deposits, nil) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d1.DestinationDomainID, + d1.DepositNonce, + d1.ResourceID, + d1.Data, + d1.HandlerResponse, + ).Return( + &types.Message{DepositNonce: 1}, + nil, + ) + s.mockDepositHandler.EXPECT().HandleDeposit( + s.domainID, + d2.DestinationDomainID, + d2.DepositNonce, + d2.ResourceID, + d2.Data, + d2.HandlerResponse, + ).Return( + &types.Message{DepositNonce: 2}, + nil, + ) + + msgChan := make(chan []*types.Message, 2) + err := s.depositEventHandler.HandleEvent(big.NewInt(0), big.NewInt(5), msgChan) + msgs := <-msgChan + + s.Nil(err) + s.Equal(msgs, []*types.Message{{DepositNonce: 1}, {DepositNonce: 2}}) +} diff --git a/chains/evm/eventhandlers/mock/eventhandlers.go b/chains/evm/eventhandlers/mock/eventhandlers.go new file mode 100644 index 00000000..dabd3faa --- /dev/null +++ b/chains/evm/eventhandlers/mock/eventhandlers.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./chains/evm/eventhandlers/event-handler.go +// +// Generated by this command: +// +// mockgen -destination=chains/evm/eventhandlers/mock/eventhandlers.go -source=./chains/evm/eventhandlers/event-handler.go -package mock +// +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + big "math/big" + reflect "reflect" + + eventhandlers "github.com/ChainSafe/sygma-core/chains/evm/eventhandlers" + types "github.com/ChainSafe/sygma-core/types" + common "github.com/ethereum/go-ethereum/common" + gomock "go.uber.org/mock/gomock" +) + +// MockEventListener is a mock of EventListener interface. +type MockEventListener struct { + ctrl *gomock.Controller + recorder *MockEventListenerMockRecorder +} + +// MockEventListenerMockRecorder is the mock recorder for MockEventListener. +type MockEventListenerMockRecorder struct { + mock *MockEventListener +} + +// NewMockEventListener creates a new mock instance. +func NewMockEventListener(ctrl *gomock.Controller) *MockEventListener { + mock := &MockEventListener{ctrl: ctrl} + mock.recorder = &MockEventListenerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEventListener) EXPECT() *MockEventListenerMockRecorder { + return m.recorder +} + +// FetchDeposits mocks base method. +func (m *MockEventListener) FetchDeposits(ctx context.Context, address common.Address, startBlock, endBlock *big.Int) ([]*eventhandlers.Deposit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchDeposits", ctx, address, startBlock, endBlock) + ret0, _ := ret[0].([]*eventhandlers.Deposit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchDeposits indicates an expected call of FetchDeposits. +func (mr *MockEventListenerMockRecorder) FetchDeposits(ctx, address, startBlock, endBlock any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchDeposits", reflect.TypeOf((*MockEventListener)(nil).FetchDeposits), ctx, address, startBlock, endBlock) +} + +// MockDepositHandler is a mock of DepositHandler interface. +type MockDepositHandler struct { + ctrl *gomock.Controller + recorder *MockDepositHandlerMockRecorder +} + +// MockDepositHandlerMockRecorder is the mock recorder for MockDepositHandler. +type MockDepositHandlerMockRecorder struct { + mock *MockDepositHandler +} + +// NewMockDepositHandler creates a new mock instance. +func NewMockDepositHandler(ctrl *gomock.Controller) *MockDepositHandler { + mock := &MockDepositHandler{ctrl: ctrl} + mock.recorder = &MockDepositHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDepositHandler) EXPECT() *MockDepositHandlerMockRecorder { + return m.recorder +} + +// HandleDeposit mocks base method. +func (m *MockDepositHandler) HandleDeposit(sourceID, destID uint8, nonce uint64, resourceID types.ResourceID, calldata, handlerResponse []byte) (*types.Message, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HandleDeposit", sourceID, destID, nonce, resourceID, calldata, handlerResponse) + ret0, _ := ret[0].(*types.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HandleDeposit indicates an expected call of HandleDeposit. +func (mr *MockDepositHandlerMockRecorder) HandleDeposit(sourceID, destID, nonce, resourceID, calldata, handlerResponse any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleDeposit", reflect.TypeOf((*MockDepositHandler)(nil).HandleDeposit), sourceID, destID, nonce, resourceID, calldata, handlerResponse) +} diff --git a/go.mod b/go.mod index a1836255..8291140c 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( ) require ( + github.com/ChainSafe/chainbridge-core v1.4.1 // indirect github.com/ChainSafe/go-schnorrkel v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/bits-and-blooms/bitset v1.7.0 // indirect diff --git a/go.sum b/go.sum index a1a1a6fe..b6443acf 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,9 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ChainSafe/chainbridge-core v1.4.1 h1:ks2ViOlduDq4p4z01BS6pnrPYR5IYPP8Fwfo5YP8MxY= +github.com/ChainSafe/chainbridge-core v1.4.1/go.mod h1:4WieCRk3wbrtiRZPTpYjXlOTbZLiRTptWiTEsO+yhnc= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= diff --git a/types/message.go b/types/message.go new file mode 100644 index 00000000..aa0c1a99 --- /dev/null +++ b/types/message.go @@ -0,0 +1,51 @@ +package types + +import ( + "strconv" +) + +type Metadata struct { + Data map[string]interface{} +} +type TransferType string +type ResourceID [32]byte + +const ( + FungibleTransfer TransferType = "FungibleTransfer" + NonFungibleTransfer TransferType = "NonFungibleTransfer" + GenericTransfer TransferType = "GenericTransfer" +) + +type Message struct { + Source uint8 // Source where message was initiated + Destination uint8 // Destination chain of message + DepositNonce uint64 // Nonce for the deposit + ResourceId ResourceID + Payload []interface{} // data associated with event sequence + Metadata Metadata // Arbitrary data that will be most likely be used by the relayer + Type TransferType +} + +func NewMessage( + source uint8, + destination uint8, + depositNonce uint64, + resourceId ResourceID, + transferType TransferType, + payload []interface{}, + metadata Metadata, +) *Message { + return &Message{ + source, + destination, + depositNonce, + resourceId, + payload, + metadata, + transferType, + } +} + +func (m Message) ID() string { + return strconv.FormatInt(int64(m.Source), 10) + "-" + strconv.FormatInt(int64(m.Destination), 10) + "-" + strconv.FormatInt(int64(m.DepositNonce), 10) +}