Skip to content

fix(mev): send BlockRazor its own method on every submit path, not just SendBundleV2 - #229

Closed
datluongductuan wants to merge 1 commit into
mainfrom
fix/blockrazor-method-on-sendbundle
Closed

datluongductuan wants to merge 1 commit into
mainfrom
fix/blockrazor-method-on-sendbundle

Conversation

@datluongductuan

Copy link
Copy Markdown
Member

Found while wiring the BSC builders into dexdex-arb, which is what makes it
concrete rather than theoretical.

The bug

#227 taught SendBundleV2 that BlockRazor's free RPC serves eth_sendMevBundle.
SendBundle and SendBundleHex kept hardcoding eth_sendBundle:

func (s *Client) SendBundle(...) (SendBundleResponse, error) {
	return s.sendBundle(ctx, ETHSendBundleMethod, uuid, blockNumber, txs, nil)
}

So the two entry points disagreed about the protocol. A caller on the plain path
reaches a method bsc.blockrazor.xyz does not implement — the bundle never
lands and the send looks fine.
There is no error to see, which is the worst
shape this class of bug comes in.

This is not hypothetical: dexdex-arb's broadcaster calls SendBundle
(internal/executor/broadcaster/broadcaster.go:288). Wiring BlockRazor there
against the current library would have submitted the wrong method from the very
first bundle, and it would have looked healthy.

The fix

The choice moves into submitBundleMethod() and is applied at the three submit
entry points — SendBundle, SendBundleHex, SendBundleV2.

Deliberately not inside sendBundle. Two of its callers pass a method of
their own:

caller method must keep
CancelBundle eth_sendBundle a cancel is not a submit
SimulateBundle eth_callBundle rewriting it turns a simulation into a send

Pushing the swap one level down would have been smaller and wrong.

Tests

The method is pinned on both plain paths, for BlockRazor and for a control
sender, plus a case that eth_callBundle survives.

Verified by mutation rather than assumed — flattening submitBundleMethod to the
constant fails:

[FAIL] TestSendBundle_BlockRazorTakesTheFreeRPCMethod/SendBundleHex
       expected: "eth_sendMevBundle"  actual: "eth_sendBundle"
[FAIL] TestSendBundleV2_BlockRazorTakesNeitherFieldNorTheBuilderMethod
       expected: "eth_sendMevBundle"  actual: "eth_sendBundle"

The existing #227 test failing too is the useful half: it shows the refactor did
not quietly change the V2 path it was meant to preserve.

go test ./pkg/mev/... 30 passed. golangci-lint --new-from-rev=origin/main
clean (the 7 pre-existing issues in this package are untouched).

Not addressed here

SendBundle still omits the 48spSign body field that SendBundleV2 sets for
48Club: Sign48SPMember needs the parsed transactions, and SendBundleHex has
only their hex. A 48Club bundle sent via SendBundle is a valid anonymous
bundle — it just forgoes Soul Point priority. Worth a follow-up, not a
correctness break.

🤖 Generated with Claude Code

…st SendBundleV2

#227 taught SendBundleV2 that BlockRazor's free RPC serves eth_sendMevBundle, but
SendBundle and SendBundleHex still hardcoded eth_sendBundle. The two entry points
therefore disagreed about the protocol: a caller on the plain path reached a
method bsc.blockrazor.xyz does not implement, so the bundle never lands and the
send itself looks fine — there is no error to see.

That is not hypothetical. dexdex-arb's broadcaster calls SendBundle, so wiring
BlockRazor there against the current library would have submitted the wrong
method from the first bundle.

The choice moves into submitBundleMethod() and is applied at the three submit
entry points. Deliberately NOT inside sendBundle: CancelBundle and
SimulateBundle pass a method of their own (eth_sendBundle for a cancel,
eth_callBundle for a simulation), and rewriting those would turn a simulation
into a submission.

Tests pin the method on both plain paths, for BlockRazor and for a control
sender, and pin that eth_callBundle survives. Verified by mutation: flattening
submitBundleMethod to the constant fails the new SendBundleHex case and the
existing SendBundleV2 one, so the refactor is covered from both directions.

Not addressed here: SendBundle still omits the 48spSign body field that
SendBundleV2 sets for 48Club, because Sign48SPMember needs the parsed
transactions and SendBundleHex has only their hex. A 48Club bundle sent via
SendBundle is a valid anonymous bundle — it just forgoes Soul Point priority.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@datluongductuan

Copy link
Copy Markdown
Member Author

Closing — superseded by a fix on the consumer side.

SendBundleV2 is already on IBundleSender and SendBundleV2Request carries both UUID and BlockNumber, so dexdex-arb can select the right call itself without a library change:

if sender.GetSenderType() == mev.BundleSenderTypeBlockRazor {
    return sender.SendBundleV2(ctx, mev.SendBundleV2Request{UUID: bUUID, BlockNumber: &blockNum}, bundle...)
}
return sender.SendBundle(ctx, bUUID, blockNum, bundle...)

Verified end-to-end against stock v0.11.32 through the real RegisterBuilders → send path: BlockRazor now emits eth_sendMevBundle, and Flashbots/48Club keep eth_sendBundle. KyberNetwork/dexdex-arbitrage#457.

That unblocks BSC now rather than waiting on a release here, and keeps the blast radius to one service instead of every tradinglib consumer.

Worth noting the underlying asymmetry is still there: SendBundle and SendBundleHex hardcode eth_sendBundle, so any other consumer reaching BlockRazor through them hits the same silent failure — the bundle never lands and the send reports success. Reopen this if that is worth closing centrally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant