Skip to content

security(high): refund nonce derives from refundableAmount — nonce collision permanently bricks refunds #36

Description

@pviti

Severity: High. From the 2026-07-29 cross-repo audit.

The refund nonce is derived from the current refundable balance (contracts/src/RAIL0.sol:480, _refundNonce at :566-572):

_refundNonce(paymentId, _configHash[paymentId], st.refundableAmount)
// = keccak256(abi.encode(_REFUND_NONCE_PREFIX, paymentId, configHash, refundableAmount))

It includes neither the refund amount nor a counter — and refundableAmount is not monotonic: capture increases it (:391).

Failure scenario (verified against the code): authorize 100 → capture(50) (refundable = 50) → refund(50) (token permanently burns nonce N(…, 50); refundable = 0) → capture(50) (refundable = 50 again) → any subsequent refund derives the same N(…, 50) and the token reverts "authorization is used". The payee cannot vary the amount to escape (amount isn't in the nonce) and cannot change the state once capturableAmount is exhausted: that residual is permanently non-refundable, and an open dispute on it can never be resolved by the merchant.

RAIL0.sol:39-41 and README.md:157 assert nonce uniqueness "per partial refund", which only holds for a strictly-decreasing balance. No test exercises a revisited refundableAmount (test_Refund_Partial_Multiple uses 100 → 70 → 0, all distinct).

Suggested fix: include a per-payment refund counter (or counter + amount) in the nonce derivation. Fund-moving change → version bump + coordinated rollout. Until then the gateway can guard the sequence app-side (refuse a refund whose derived nonce was already consumed — authorizationState is even declared in IEIP3009).

Metadata

Metadata

Assignees

No one assigned

    Labels

    perf-regressesFixing this COSTS performance — a deliberate trade-offperformanceAffects performance, positively or negatively (latency, load, gas, queries, bundle size)securityVanta Reservedsecurity-improvesFixing this STRENGTHENS security

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions