Skip to content

feat: add use-refund-protocol skill#12

Open
OliverDevDS wants to merge 2 commits into
circlefin:masterfrom
OliverDevDS:feat/use-refund-protocol
Open

feat: add use-refund-protocol skill#12
OliverDevDS wants to merge 2 commits into
circlefin:masterfrom
OliverDevDS:feat/use-refund-protocol

Conversation

@OliverDevDS
Copy link
Copy Markdown

What this PR adds

A new skill use-refund-protocol documenting the Circle Refund Protocol — non-custodial escrow with arbiter-mediated dispute resolution for stablecoin payments on Arc.

Motivation

The circlefin/refund-protocol repository and arc-escrow sample app exist but no skill documents the integration pattern. This skill fills that gap.

What's included

  • SKILL.md (537 lines) covering all 10 contract functions
  • Three refund paths: voluntary, forced, early release (EIP-712)
  • Arbiter fund management and debt settlement
  • 6 antipatterns with WRONG/CORRECT examples
  • Decision guide mapping situations to refund paths
  • Use cases: merchant chargebacks, gig economy, AI-validated escrow

Closes #11


Submitted via Claude Code + WSL using the Circle MCP server.

Documents the Circle Refund Protocol — non-custodial escrow with
arbiter-mediated dispute resolution for stablecoin payments on Arc.

Covers: pay/withdraw lifecycle, lockup periods, refundByRecipient,
refundByArbiter, earlyWithdrawByArbiter (EIP-712), arbiter fund
management, debt settlement, updateRefundTo, and 6 antipatterns.
@OliverDevDS OliverDevDS force-pushed the feat/use-refund-protocol branch from 989b7f4 to 41a2134 Compare April 9, 2026 04:33
```

When the arbiter covers a refund from its own balance, the recipient incurs
a debt. The debt is automatically settled when the recipient next receives
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This debt-settlement behavior is not what the contract does. pay() does not call _settleDebt(to); it transfers funds in, records the payment, increments balances[to], emits PaymentCreated, and increments nonce. Debt is only settled through settleDebt(recipient) or when withdraw() calls _settleDebt(msg.sender).

The repo’s own tests confirm this: after a later pay(), debts(receiver) is still 100 and balances(receiver) is 100 until settlement runs.

A safer wording would be: new payments increase the recipient’s balance, but existing debt remains until settleDebt(recipient) is called or the recipient attempts withdraw(). As written, builders may assume arbiter exposure is repaid automatically on new payments when it is not.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — corrected the description to clarify that pay() does not settle debt automatically. Debt is only cleared via explicit settleDebt(recipient) or when the recipient calls withdraw().

// Funds go to the refundTo address — recipient cannot block this
```

### 6. Early withdrawal authorized by arbiter
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section should not present earlyWithdrawByArbiter() as ready-to-copy integration guidance in its current form. The current refund-protocol README has an active Security Notice for the early withdrawal function: it says the issue allows an arbiter to drain other users’ payments and that a fix is still in development.

There is also a concrete signature mismatch in this sample. signTypedData() produces canonical EIP-712 array encoding, but the contract verifies ecrecover(_hashEarlyWithdrawalInfo(...)), and _hashEarlyWithdrawalInfo() hashes paymentIDs and withdrawalAmounts with abi.encode(...) directly. I checked this with viem@2.44.4; the typed-data digest and the contract digest differ for the same inputs, so the signature produced by this snippet will not verify.

I’d either remove this integration section until the upstream contract is fixed, or clearly mark it unsafe and replace the signing flow with one that exactly matches hashEarlyWithdrawalInfo().

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added a security warning about the active upstream issue and replaced signTypedData() with encodeAbiParameters + keccak256 + sign to match _hashEarlyWithdrawalInfo() exactly.

functionName: "nonce",
});

// Step 3: Pay — funds go into escrow
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This flow is titled “Basic payment with lockup,” but the payment here does not actually use LOCKUP_SECONDS. The contract reads lockupSeconds[to] inside pay(), and that mapping defaults to 0 unless the arbiter already called setLockupSeconds(recipient, ...) before the payment.

Following this section in order creates an immediately withdrawable payment, while the log below still prints a 7-day lockup estimate. I’d move the setLockupSeconds step before pay(), or mark it as a prerequisite and read the actual releaseTimestamp from payments(paymentId) after the transaction.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — moved setLockupSeconds before pay() in the flow and replaced the estimated timestamp log with a read from payments(paymentId).releaseTimestamp.

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.

New skill proposal: use-refund-protocol

2 participants