Skip to content

fix(fund): assign multicall3 nonces up front to stop batches replacing each other - #1030

Merged
praetoriansentry merged 1 commit into
mainfrom
fix/fund-multicall3-nonce-race
Sep 29, 2026
Merged

praetoriansentry merged 1 commit into
mainfrom
fix/fund-multicall3-nonce-race

Conversation

@praetoriansentry

Copy link
Copy Markdown
Member

Description

polycli fund sends one multicall3 transaction per batch of wallets, each from its own goroutine. Every goroutine left Nonce nil, so each one asked the node for the pending nonce on its own. Two batches that read the same value collided: the second broadcast was rejected with replacement transaction underpriced and its wallets were never funded. If the second batch's fee estimate happened to come out higher, it instead silently replaced the first, and the receipt wait blocked on a hash that would never be mined. A 10,000-wallet run lost three of 25 batches this way.

Changes:

  • Read the funder's pending nonce once and preassign a nonce to every batch. The concurrent fan-out stays, but the goroutines no longer touch the nonce endpoint, so neither timing nor load-balanced RPC routing matters.
  • Retry each send up to three times with backoff before treating its nonce as a gap.
  • When a send still fails, confirm the transactions below the gap, then return an error listing the batches that were not sent, how many accounts were not funded, and that the higher-nonce transactions stay pending until the gap is filled (pointing at polycli fix-nonce-gap). Previously a single send failure returned before confirming anything.
  • ERC20 mode: a single approval for the total amount now covers every batch, instead of one approval per batch that each waited to be mined. util.Multicall3FundAccountsWithERC20Token no longer approves; the new util.Multicall3ApproveERC20Token does, and it returns an error when the approval is mined but fails instead of returning nil.
  • Progress logs now report batch=N of=M nonce=... accounts=... instead of a wallet count that arrived out of order.

Breaking: the signature of util.Multicall3FundAccountsWithERC20Token changed. Its only caller was cmd/fund.

Jira / Linear Tickets

  • N/A

Testing

  • go build ./..., go vet ./..., shadow analyzer on changed packages
  • New unit tests for batch splitting, gap detection, and the failure summary (go test -race ./cmd/fund/)
  • Manual run against a live network with --key-file of 10,000 wallets (native token), confirming all 25 batches land and no replacement transaction underpriced errors
  • Manual run in ERC20 mode (--token-address) confirming a single approval followed by the batch transfers

🤖 Generated with Claude Code

…g each other

fundWalletsWithMulticall3 sent every batch from its own goroutine with a
nil nonce, so each goroutine asked the node for the pending nonce
independently. Two batches that read the same value collided: the second
broadcast was rejected with "replacement transaction underpriced" and its
wallets were never funded, or, if its fee estimate happened to be higher,
it silently replaced the first and the receipt wait blocked on a hash that
would never be mined.

Read the pending nonce once, hand each batch its own nonce, and keep the
concurrent fan-out. Retry each send with backoff before treating its nonce
as a gap. When a send still fails, confirm the transactions below the gap,
then report which batches were not sent and that the higher-nonce
transactions stay pending until the gap is filled.

In ERC20 mode a single approval for the total amount now covers every
batch instead of one approval per batch that each waited to be mined.
Multicall3FundAccountsWithERC20Token no longer approves, and the new
Multicall3ApproveERC20Token returns an error when the approval is mined
but fails instead of returning nil.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@praetoriansentry
praetoriansentry merged commit 5a1e6fd into main Sep 29, 2026
15 checks passed
@praetoriansentry
praetoriansentry deleted the fix/fund-multicall3-nonce-race branch September 29, 2026 15:06
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.

2 participants