Skip to content

docs(fees): document per-capture rounding and fragmentation risk (audit follow-up to #90)#92

Merged
amiecorso merged 2 commits into
mainfrom
audit-2
Jul 21, 2026
Merged

docs(fees): document per-capture rounding and fragmentation risk (audit follow-up to #90)#92
amiecorso merged 2 commits into
mainfrom
audit-2

Conversation

@RuoHan-Chen

@RuoHan-Chen RuoHan-Chen commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Docs-only follow-up addressing three audit findings on merged PR #90. All three are documentation clarifications; the deployed contract at 0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff is unchanged and no code, ABI, tests, or CI are affected.

Findings addressed

1. Per-capture rounding and fragmentation risk (commit 8026382)

Documents that minFeeBps / maxFeeBps are per-capture rate bounds (not aggregate payment-level guarantees) and that integer division floors both minFee and maxFee. This enables a fragmentation edge case where an operator with capture-amount discretion can split a payment into many small captures whose individual minFee values each round to zero.

This behavior is pre-existing — the old feeBps API used the same amount * bps / 10_000 per-capture formula. PR #90 preserved the semantics while moving the concrete fee amount off-chain, so this is not a regression from the absolute-fee migration.

Accepted-risk rationale:

  • Operators are trusted counterparties and, in current deployments, are typically also the fee receiver — fragmenting fees away from themselves has no benefit.
  • Per-capture gas costs make fragmenting economically pointless except for low-decimal, high-unit-value tokens.
  • USDC's 6 decimals push the rounding gap below one cent at typical bps rates.
  • Tightening on-chain would require either per-payment aggregate fee accounting or a minimum-capture-size rule, both of which are separate design decisions that would change the two-phase escrow's flexibility.

2. Example 4 contradicted _validateFee (commit f6c2618)

docs/Fees.md Example 4 previously listed feeAmount: 0, feeReceiver: address(0) as valid when PaymentInfo.feeReceiver is a fixed non-zero address. The actual _validateFee logic (src/AuthCaptureEscrow.sol:511–530) reverts with InvalidFeeReceiver in that case — receiver matching is unconditional, not gated on feeAmount > 0. Receiver skipping only applies under the flexible-recipient branch (PaymentInfo.feeReceiver == address(0), Example 3).

Fix: remove the incorrect row, add it as an explicit failure case, and add a callout note distinguishing fixed vs. flexible recipient semantics. Docs are now aligned with deployed behavior.

3. topic0 migration for PaymentCharged and PaymentCaptured (commit f6c2618)

The uint16 feeBpsuint256 feeAmount change altered the Solidity event signatures, producing brand-new topic0 values for both PaymentCharged and PaymentCaptured. Off-chain indexers filtering by the old topic0 will silently observe zero matching logs from a new deployment — no error surfaces. Worse, an old ABI applied to the new logs decodes without error but misinterprets the fee (e.g. a 25e6 USDC absolute fee is read as 25,000,000 bps).

The current deployment (0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff) is immutable and continues to emit the old signatures. Any future redeployment will emit the new signatures. This can't be avoided in Solidity (topic0 is derived from the canonical signature), so this is purely a release-management/integrator-communication concern.

Fix: add a new "Event Migration: topic0 Changes" section publishing both sets of canonical signatures and the four topic0 hashes, with a migration checklist recommending (address, topic0) routing during the coexistence period.

topic0 values (verified with cast keccak and independently reproduced with a fresh Python Keccak-256 impl — byte-identical):

Event Version topic0
PaymentCharged old 0x943ae4341dd799d7aeedc501f616cd26b134639e0bc2ec059581ba3ebbf1e7d0
PaymentCharged new 0x137b0e73e4453f43c2e1ad2552980a0e0c7e988764619974ca26d37a7159940c
PaymentCaptured old 0xe749f7bbd01b49bb05abf26ca492cb4dfdea6bedeada8a40fdccd478c73a74e2
PaymentCaptured new 0xac1e0db1957daedbc6944bcb4c8dc947ff8101d710671ac2ec26309f7f38d58e

Changes

docs/Fees.md only:

  • New "Per-Capture Semantics and Rounding" section (per-capture bounds, rounding direction, worked fragmentation example, pre-existing framing, accepted-risk rationale).
  • Example 4 corrected: replace the incorrect feeAmount: 0, feeReceiver: address(0) valid-row with an explicit failure case, plus a callout note on fixed vs. flexible recipient semantics.
  • New "Event Migration: topic0 Changes for PaymentCharged and PaymentCaptured" section: canonical old/new signatures, topic0 values, ABI-misdecode warning, integrator migration checklist.
  • Fix previously truncated trailing sentence at end of file.

No code changes

Docs-only. No impact on contract bytecode, ABI, tests, or CI.

Clarifies that minFeeBps/maxFeeBps are per-capture (not aggregate)
bounds and that integer division floors both minFee and maxFee.
Includes a worked fragmentation example and states this as pre-existing
accepted risk under the current operator-trust model.

Addresses audit follow-up to #90.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 1
Sum 2

…c0 migration

Two audit follow-ups on merged PR #90:

1. Example 4 previously showed feeAmount=0 with feeReceiver=address(0)
   as valid when PaymentInfo.feeReceiver is a fixed non-zero address.
   _validateFee actually reverts with InvalidFeeReceiver in that case
   (receiver matching is unconditional, not gated on feeAmount>0). Fix
   the example to require the configured recipient and add a call-out
   note distinguishing fixed vs. flexible recipient semantics.

2. Add an 'Event Migration' section documenting the topic0 changes on
   PaymentCharged and PaymentCaptured. Includes canonical old/new
   signatures, the four topic0 hashes (verified with cast and
   independently reproduced), a warning that ABIs from before the
   migration decode the new logs silently-incorrectly (feeBps vs
   feeAmount semantics at the same log data offset), and a migration
   checklist for integrators.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
@RuoHan-Chen
RuoHan-Chen requested a review from amiecorso July 21, 2026 17:25
@amiecorso
amiecorso merged commit 9436713 into main Jul 21, 2026
5 of 6 checks passed
@amiecorso
amiecorso deleted the audit-2 branch July 21, 2026 21:06
@cb-heimdall

Copy link
Copy Markdown
Collaborator

Review Error for amiecorso @ 2026-07-21 21:07:42 UTC
User failed mfa authentication, either user does not exist or public email is not set on your github profile. \ see go/mfa-help

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.

3 participants