Skip to content

fix: never split to a builder payout USDC refuses - #60

Merged
aaronjmars merged 1 commit into
mainfrom
fix/screen-builder-payout
Oct 29, 2026
Merged

aaronjmars merged 1 commit into
mainfrom
fix/screen-builder-payout

Conversation

@aaronjmars

@aaronjmars aaronjmars commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

What

A builder payout that USDC will not pay (the USDC contract itself, or an address USDC has blacklisted) could become a recipient of the per-pair PushSplit. The split pays every recipient in one distribute and is ownerless, so every distribute would revert and each payment carrying that code would stay in the split, the seller's share included.

Fix (Python and TS)

  • resolver.payout_blocked / Affiliation.payoutBlocked: the zero address and the USDC contract are blocked outright; any other payout is checked with USDC.isBlacklisted (on Base, USDC reports its own address as blacklisted too).
  • resolver.resolve returns payout_blocked; split.resolve_and_plan drops a blocked payout, so the seller is paid in full.
  • New X-Builder-Code-Status value blocked. It sits in the short-TTL negative cache like unregistered, so a fixed payout is picked up after the TTL.
  • A failed screen read falls back to the seller with status error, like other lookup failures.
  • Python Affiliation.pay_to now runs the resolve in a worker thread (asyncio.to_thread), so its blocking RPC reads no longer stall the event loop.
  • Docs list the new status.

Tests

  • Python: the real resolver, split and payto path against a fake JSON-RPC (registry, USDC, factory): good payout still splits and is screened; blacklisted payout and USDC-as-payout pay the seller with blocked and no split address is predicted; blocked codes come from the negative cache; a failed screen read gives error. Plus resolver unit tests for the screen and a test that pay_to keeps the event loop free.
  • TS: same cases through the mock transport.
  • All existing tests pass unchanged. The new ones fail on main.

Not in scope: the positive cache has no TTL, so a payout blacklisted after it was cached is only re-screened on restart or clear_cache().


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

A PushSplit pays every recipient in one distribute and is ownerless, so a
builder payout that USDC refuses (the USDC contract itself, or a blacklisted
address) would make every distribute revert and leave each payment carrying
that code stuck in the split, the seller's share too.

Screen the registry payout before building the split (Python and TS): the
zero address and the USDC contract are blocked outright, anything else is
checked with USDC.isBlacklisted. A blocked payout pays the seller in full with
a new "blocked" status, held in the short-TTL negative cache. A failed screen
read falls back to the seller with status "error", like other lookup failures.

Also run the Python Affiliation.pay_to resolve in a worker thread, so its
blocking RPC reads no longer stall the event loop.
@aaronjmars
aaronjmars merged commit e5a763a into main Oct 29, 2026
6 checks passed
@aaronjmars aaronjmars mentioned this pull request Oct 29, 2026
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