Skip to content

Refund Protocol - Debt Bypass via refundBySeller #10

Description

@lukepuplett

Note - This is out-of-scope on HackerOne for bug reports, so here I am.

Summary:

Refund Protocol - Debt Bypass via refundBySeller

https://github.com/circlefin/refund-protocol/blob/master/src/RefundProtocol.sol

The refundBySeller function does not settle outstanding seller debts before processing refunds. This allows sellers to extract funds that should first go toward repaying the arbiter, potentially causing permanent financial loss to the arbiter.

refundBySeller lacks a call to _settleDebt(msg.sender) at the beginning of the function, unlike withdraw() which correctly settles debts first.

Debts are created when the arbiter covers chargebacks/refunds on behalf of sellers who lack sufficient balance.

The only defensible interpretation would be:

"Refunds are customer-protection sacred and must bypass debt settlement."

Steps To Reproduce:

Attack Scenario (1M USDC Example)

Step 1: Seller accumulates debt

  1. Seller operates high-volume business, receives 5M USDC in payments
  2. Seller withdraws most funds: balances[seller] = 100K USDC
  3. Customer disputes/chargebacks occur totalling 1M USDC
  4. Arbiter calls refundByArbiter to cover these chargebacks
  5. Arbiter pays from their balance: balances[arbiter] -= 1M
  6. Result: debts[seller] = 1M USDC, balances[seller] = 100K USDC

Step 2: Seller receives new payment

  1. New customer pays 900K USDC for goods/services
  2. Result: balances[seller] = 1M USDC, debts[seller] = 1M USDC

Step 3: Seller exploits the vulnerability

  1. Seller (legitimately or collusively) decides to issue refund on the 900K payment
  2. Seller calls refundBySeller(newPaymentID)
  3. Code checks: 900K <= 1M ✓ passes
  4. Code executes: balances[seller] = 1M - 900K = 100K
  5. Customer receives 900K USDC refund
  6. Result: balances[seller] = 100K, debts[seller] = 1M USDC (unchanged!)

Step 4: Seller tries to exit

  1. Seller attempts withdraw() to extract remaining 100K
  2. _settleDebt(msg.sender) executes automatically
  3. 100K → arbiter, reducing debt to 900K
  4. Withdrawal fails (insufficient funds)
  5. Seller gets nothing, leaves platform
  6. Final permanent loss: 900K USDC (the amount that should have been settled but was used for refund instead)

Supporting Material/References:

None

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions