Skip to content

security(high): authorize/charge use transferWithAuthorization — front-runnable, buyer funds permanently lost #35

Description

@pviti

Severity: High. From the 2026-07-29 cross-repo audit (functional/security/perf/style pass).

authorize (contracts/src/RAIL0.sol:275) and charge (:304) pull the buyer's funds with transferWithAuthorization:

IEIP3009(p.token).transferWithAuthorization(
    p.payer, address(this), p.amount, 0, p.authorizationExpiry,
    _authorizeNonce(paymentId, configHash), v, r, s
);

EIP-3009's transferWithAuthorization has no msg.sender restrictionreceiveWithAuthorization exists in the EIP precisely to prevent third-party submission, and refund (:474) already uses it.

Failure scenario: the merchant broadcasts authorize; any mempool observer lifts (v, r, s) plus the deterministic nonce from the pending tx and calls token.transferWithAuthorization(payer, RAIL0, amount, 0, expiry, nonce, v, r, s) directly with a higher tip. The token moves amount from the buyer to the RAIL0 address and marks the nonce used forever; the merchant's authorize then reverts token-side, so no PaymentState is ever created. There is no sweep/rescue function and every payout path pays out only against a live _state[paymentId], so the balance is unrecoverable — pure griefing, the buyer loses the full amount, attacker cost is gas. Same applies to charge.

Suggested fix: switch both call sites to receiveWithAuthorization (the buyer signs the identical TransferWithAuthorization digest; to == msg.sender == RAIL0 then holds). Fund-moving change → version bump + full coordinated rollout (one version across all active chains). The README security model (README.md:288-300) should also mention mempool front-running explicitly.

Metadata

Metadata

Assignees

Labels

securityVanta Reservedsecurity-improvesFixing this STRENGTHENS securitywontfixThis will not be worked on

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions