diff --git a/pkg/mev/backrun_public_sender.go b/pkg/mev/backrun_public_sender.go index ed2005e..791dfc1 100644 --- a/pkg/mev/backrun_public_sender.go +++ b/pkg/mev/backrun_public_sender.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "encoding/json" "fmt" + "math/big" "net/http" "github.com/ethereum/go-ethereum/common" @@ -44,6 +45,7 @@ func (b BackrunPublicClient) SendBackrunBundle( _ uint64, pendingTxHashes []common.Hash, _ []string, + _ *big.Int, txs ...*types.Transaction, ) (SendBundleResponse, error) { req := SendRequest{ diff --git a/pkg/mev/backrun_public_sender_test.go b/pkg/mev/backrun_public_sender_test.go index 9822a89..5708f5f 100644 --- a/pkg/mev/backrun_public_sender_test.go +++ b/pkg/mev/backrun_public_sender_test.go @@ -41,7 +41,7 @@ func TestNewBackrunPublicClient(t *testing.T) { pendingTXhash := common.HexToHash("0x79d48b1a25d7af0d815997d2ce3a127560080971c5ea98ca5a32424f604e09fb") resp, err := senderClient.SendBackrunBundle(context.Background(), nil, - blockNumber, blockNumber, []common.Hash{pendingTXhash}, []string{}, tx) + blockNumber, blockNumber, []common.Hash{pendingTXhash}, []string{}, nil, tx) t.Log("resp", resp) t.Log("err", err) } diff --git a/pkg/mev/bloxroute_backrunme_sender.go b/pkg/mev/bloxroute_backrunme_sender.go index bb3eda8..7f50923 100644 --- a/pkg/mev/bloxroute_backrunme_sender.go +++ b/pkg/mev/bloxroute_backrunme_sender.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "math/big" "net/http" "strings" @@ -35,6 +36,8 @@ type backrunmeRequestParams struct { Timestamp *uint64 `json:"timestamp,omitempty"` MinTimestamp *uint64 `json:"min_timestamp,omitempty"` MaxTimestamp *uint64 `json:"max_timestamp,omitempty"` + // CoinbaseProfit is the wei paid to the BackRunMe contract (a JSON number). + CoinbaseProfit *big.Int `json:"coinbase_profit,omitempty"` } type backrunmeResponse struct { @@ -130,6 +133,7 @@ func (s *BloxrouteBackrunmeSender) SendBackrunBundle( maxBlockNumber uint64, pendingTxHashes []common.Hash, targetBuilders []string, + coinbaseProfit *big.Int, txs ...*types.Transaction, ) (SendBundleResponse, error) { // Validate inputs @@ -165,6 +169,7 @@ func (s *BloxrouteBackrunmeSender) SendBackrunBundle( TransactionHash: pendingTxHashes[0].Hex(), Transaction: transactions, BlockNumber: fmt.Sprintf("0x%x", blockNumber), + CoinbaseProfit: coinbaseProfit, } // Build request diff --git a/pkg/mev/bloxroute_backrunme_sender_test.go b/pkg/mev/bloxroute_backrunme_sender_test.go index cb7285e..767be1b 100644 --- a/pkg/mev/bloxroute_backrunme_sender_test.go +++ b/pkg/mev/bloxroute_backrunme_sender_test.go @@ -33,7 +33,7 @@ func TestBloxrouteBackrunmeSender_SendBackrunBundle(t *testing.T) { pendingTXhash := common.HexToHash("0x79d48b1a25d7af0d815997d2ce3a127560080971c5ea98ca5a32424f604e09fb") - resp, err := sender.SendBackrunBundle(context.Background(), nil, 1, 1, []common.Hash{pendingTXhash}, []string{}, tx) + resp, err := sender.SendBackrunBundle(context.Background(), nil, 1, 1, []common.Hash{pendingTXhash}, []string{}, nil, tx) require.NoError(t, err) t.Log("resp", resp.Result.BundleHash) } diff --git a/pkg/mev/merkle_sender.go b/pkg/mev/merkle_sender.go index 1a74f05..fbd9785 100644 --- a/pkg/mev/merkle_sender.go +++ b/pkg/mev/merkle_sender.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "encoding/json" "fmt" + "math/big" "net/http" "github.com/ethereum/go-ethereum/common" @@ -44,6 +45,7 @@ func (b MerkleClient) SendBackrunBundle( _ uint64, pendingTxHashes []common.Hash, _ []string, + _ *big.Int, txs ...*types.Transaction, ) (SendBundleResponse, error) { req := SendRequest{ diff --git a/pkg/mev/mevshare_sender.go b/pkg/mev/mevshare_sender.go index 6190177..a94d374 100644 --- a/pkg/mev/mevshare_sender.go +++ b/pkg/mev/mevshare_sender.go @@ -3,6 +3,7 @@ package mev import ( "context" "crypto/ecdsa" + "math/big" "github.com/duoxehyon/mev-share-go/rpc" "github.com/ethereum/go-ethereum/common" @@ -39,6 +40,7 @@ func (m FlashbotMevShareSender) SendBackrunBundle( maxBlockNumber uint64, pendingTxHashes []common.Hash, targetBuilders []string, + _ *big.Int, txs ...*types.Transaction, ) (SendBundleResponse, error) { if m.client == nil { diff --git a/pkg/mev/mevshare_sender_test.go b/pkg/mev/mevshare_sender_test.go index fface98..5699d15 100644 --- a/pkg/mev/mevshare_sender_test.go +++ b/pkg/mev/mevshare_sender_test.go @@ -45,7 +45,7 @@ func TestSendBackrunBundle(t *testing.T) { // Send bundle res, err := rpcClient.SendBackrunBundle(context.Background(), nil, - blockNumber+1, blockNumber+1, []common.Hash{pendingTxHash}, nil, tx) + blockNumber+1, blockNumber+1, []common.Hash{pendingTxHash}, nil, nil, tx) assert.Nil(t, err) t.Log(res, "result") diff --git a/pkg/mev/pkg.go b/pkg/mev/pkg.go index dbbb98c..a718004 100644 --- a/pkg/mev/pkg.go +++ b/pkg/mev/pkg.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "math/big" "net/http" "strings" "time" @@ -91,6 +92,9 @@ type IBackrunSender interface { maxBlockNumber uint64, pendingTxHashes []common.Hash, targetBuilders []string, + // coinbaseProfit is the wei amount paid to the BackRunMe contract, reported to + // bloxroute via submit_arb_only_bundle. nil for non-bloxroute senders (ignored). + coinbaseProfit *big.Int, tx ...*types.Transaction, ) (SendBundleResponse, error) // MevSimulateBundle only use for backrun simulate with pending tx hash diff --git a/pkg/types/bloxroute_backrun_config.go b/pkg/types/bloxroute_backrun_config.go new file mode 100644 index 0000000..e5cc1c8 --- /dev/null +++ b/pkg/types/bloxroute_backrun_config.go @@ -0,0 +1,91 @@ +package types + +import ( + "errors" + "fmt" + "strconv" + + "github.com/ethereum/go-ethereum/common" +) + +var ErrInvalidBloxrouteBackrunConfig = errors.New("invalid bloxroute backrun config") + +// BloxrouteBackrunConfig is the per-tx BackRunMe profit-split config from bloxroute's +// arbOnlyMEV stream. On the wire from bloxroute it arrives as a 5-element string array +// [contractSplit, targetRewardAddress, targetSplit, blxrRewardAddress, searcherSplit]; +// mempool-explorer parses it once via ParseBloxrouteBackrunConfig and attaches the typed +// value to Message.BloxrouteBackrunConfig, so downstream consumers read it directly (nil +// when the tx is not a bloxroute backrun). +type BloxrouteBackrunConfig struct { + ContractSplit float64 `json:"contract_split"` + TargetRewardAddress string `json:"target_reward_address"` + TargetSplit float64 `json:"target_split"` + BlxrRewardAddress string `json:"blxr_reward_address"` + SearcherSplit float64 `json:"searcher_split"` +} + +// ParseBloxrouteBackrunConfig parses the 5-element backrunConfig array from the arbOnlyMEV +// stream into a validated BloxrouteBackrunConfig. Element order is +// [contractSplit, targetRewardAddress, targetSplit, blxrRewardAddress, searcherSplit]. +func ParseBloxrouteBackrunConfig(parts []string) (*BloxrouteBackrunConfig, error) { + if len(parts) != 5 { + return nil, fmt.Errorf("%w: expected 5 elements, got %d", ErrInvalidBloxrouteBackrunConfig, len(parts)) + } + + contractSplit, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return nil, fmt.Errorf("%w: contract split: %w", ErrInvalidBloxrouteBackrunConfig, err) + } + targetSplit, err := strconv.ParseFloat(parts[2], 64) + if err != nil { + return nil, fmt.Errorf("%w: target split: %w", ErrInvalidBloxrouteBackrunConfig, err) + } + searcherSplit, err := strconv.ParseFloat(parts[4], 64) + if err != nil { + return nil, fmt.Errorf("%w: searcher split: %w", ErrInvalidBloxrouteBackrunConfig, err) + } + + cfg := BloxrouteBackrunConfig{ + ContractSplit: contractSplit, + TargetRewardAddress: parts[1], + TargetSplit: targetSplit, + BlxrRewardAddress: parts[3], + SearcherSplit: searcherSplit, + } + if err := cfg.Validate(); err != nil { + return nil, err + } + + return &cfg, nil +} + +// Validate checks the split percentages are non-negative, sum to <= 100, and the reward +// addresses are valid non-zero addresses. +func (c BloxrouteBackrunConfig) Validate() error { + if c.ContractSplit < 0 || c.TargetSplit < 0 || c.SearcherSplit < 0 { + return fmt.Errorf("%w: splits must be non-negative", ErrInvalidBloxrouteBackrunConfig) + } + if _, err := c.BlxrSplit(); err != nil { + return err + } + if !isValidNonZeroAddress(c.TargetRewardAddress) { + return fmt.Errorf("%w: invalid target reward address", ErrInvalidBloxrouteBackrunConfig) + } + if !isValidNonZeroAddress(c.BlxrRewardAddress) { + return fmt.Errorf("%w: invalid blxr reward address", ErrInvalidBloxrouteBackrunConfig) + } + return nil +} + +// BlxrSplit returns bloxroute's reward share: 100 - contract - target - searcher. +func (c BloxrouteBackrunConfig) BlxrSplit() (float64, error) { + blxrSplit := 100 - c.ContractSplit - c.TargetSplit - c.SearcherSplit + if blxrSplit < 0 { + return 0, fmt.Errorf("%w: split sum exceeds 100", ErrInvalidBloxrouteBackrunConfig) + } + return blxrSplit, nil +} + +func isValidNonZeroAddress(addr string) bool { + return common.IsHexAddress(addr) && common.HexToAddress(addr) != (common.Address{}) +} diff --git a/pkg/types/bloxroute_backrun_config_test.go b/pkg/types/bloxroute_backrun_config_test.go new file mode 100644 index 0000000..56c5355 --- /dev/null +++ b/pkg/types/bloxroute_backrun_config_test.go @@ -0,0 +1,53 @@ +package types_test + +import ( + "errors" + "testing" + + "github.com/KyberNetwork/tradinglib/pkg/types" + "github.com/stretchr/testify/require" +) + +func TestParseBloxrouteBackrunConfig(t *testing.T) { + const ( + targetAddress = "0x435D8cB64ba4B68f6A28405D3C19E7C169B01917" + blxrAddress = "0xF1Ce037Cc1d02c046e800C3265D2Efa91940864E" + ) + + cfg, err := types.ParseBloxrouteBackrunConfig([]string{"50", targetAddress, "15", blxrAddress, "20"}) + require.NoError(t, err) + require.Equal(t, 50.0, cfg.ContractSplit) + require.Equal(t, targetAddress, cfg.TargetRewardAddress) + require.Equal(t, 15.0, cfg.TargetSplit) + require.Equal(t, blxrAddress, cfg.BlxrRewardAddress) + require.Equal(t, 20.0, cfg.SearcherSplit) + + blxr, err := cfg.BlxrSplit() + require.NoError(t, err) + require.Equal(t, 15.0, blxr) // 100 - 50 - 15 - 20 +} + +func TestParseBloxrouteBackrunConfigErrors(t *testing.T) { + const ( + targetAddress = "0x435D8cB64ba4B68f6A28405D3C19E7C169B01917" + blxrAddress = "0xF1Ce037Cc1d02c046e800C3265D2Efa91940864E" + ) + + tests := []struct { + name string + parts []string + }{ + {"wrong length", []string{"50", targetAddress, "15", blxrAddress}}, + {"non-numeric split", []string{"x", targetAddress, "15", blxrAddress, "20"}}, + {"sum exceeds 100", []string{"60", targetAddress, "30", blxrAddress, "20"}}, + {"zero target address", []string{"50", "0x0000000000000000000000000000000000000000", "15", blxrAddress, "20"}}, + {"invalid blxr address", []string{"50", targetAddress, "15", "not-an-address", "20"}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := types.ParseBloxrouteBackrunConfig(tc.parts) + require.Error(t, err) + require.True(t, errors.Is(err, types.ErrInvalidBloxrouteBackrunConfig)) + }) + } +} diff --git a/pkg/types/pendingtx.go b/pkg/types/pendingtx.go index 5781e84..624110b 100644 --- a/pkg/types/pendingtx.go +++ b/pkg/types/pendingtx.go @@ -44,6 +44,9 @@ type Message struct { ExtraData string `json:"extra_data"` RawTx string `json:"raw_tx"` BlockNumber uint64 `json:"block_number"` // valid in block + // BloxrouteBackrunConfig is the per-tx BackRunMe profit-split config, set only for + // bloxroute arbOnlyMEV-sourced txs (nil otherwise). + BloxrouteBackrunConfig *BloxrouteBackrunConfig `json:"bloxroute_backrun_config,omitempty"` } type Prestate struct {