Skip to content

feat(core): delivery protection v2 — OrConditions + PaymentIndexRecorder - #105

Merged
A1igator merged 19 commits into
mainfrom
A1igator/delivery-protection-v2
Apr 2, 2026
Merged

feat(core): delivery protection v2 — OrConditions + PaymentIndexRecorder#105
A1igator merged 19 commits into
mainfrom
A1igator/delivery-protection-v2

Conversation

@A1igator

@A1igator A1igator commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Updates deployDeliveryProtectionOperator with proper access control and payment indexing.

New Operator Config

Slot Before After
releaseCondition SAC(arbiter) only OrCondition([SAC(arbiter), PayerCondition])
refundInEscrowCondition EscrowPeriod only OrCondition([EscrowPeriod, ReceiverCondition, SAC(arbiter)])
authorizeRecorder EscrowPeriod only RecorderCombinator([EscrowPeriod, PaymentIndexRecorder])

Why

  • Payer can release: satisfied payer can release without waiting for arbiter
  • Arbiter can refund immediately: on FAIL verdict, arbiter calls refundInEscrow() without waiting for escrow period
  • Receiver can refund: merchant voluntary refund
  • PaymentIndexRecorder: keepers can discover payments on-chain for independent refund triggering

Changes

  • config/index.ts: Add RecorderSingletonAddresses (paymentIndexRecorder: 0x3134...), recorderCombinatorCodehash, getRecorderSingletons()
  • deploy/presets.ts: Rewrite preview + deploy with batched OrCondition/RecorderCombinator computation
  • Tests: Full coverage for both default (6 contracts) and fallback (paymentIndexRecorderAddress: zeroAddress, 5 contracts) paths

Deploy count

6 contracts (was 3): EscrowPeriod, SAC(arbiter), OrCondition(release), OrCondition(refund), RecorderCombinator, Operator.

Test plan

  • Build passes
  • Typecheck passes
  • Biome check passes
  • Unit tests — 195 passing
  • Fork integration tests (deploy.fork.test.ts, delivery-protection.fork.test.ts)

🤖 Generated with Claude Code

Release: OrCondition([SAC(arbiter), PayerCondition]) — arbiter or payer
RefundInEscrow: OrCondition([EscrowPeriod, ReceiverCondition, SAC(arbiter)])
  — after escrow window, or receiver, or arbiter (immediate on FAIL)
AuthorizeRecorder: RecorderCombinator([EscrowPeriod, PaymentIndexRecorder])
  — graceful fallback to EscrowPeriod-only when PaymentIndexRecorder not deployed

Config additions:
- RecorderSingletonAddresses (paymentIndexRecorder)
- recorderCombinatorCodehash (Hex)
- getRecorderSingletons() getter
- Both set to zeroAddress/pad('0x00') until infra deployment

Deploy changes:
- 5 contracts (was 3): EscrowPeriod, SAC(arbiter), OrCondition(release),
  OrCondition(refund), Operator (+ RecorderCombinator when PIR available)
- New return fields: releaseConditionAddress, refundInEscrowConditionAddress,
  authorizeRecorderAddress, paymentIndexRecorderAddress

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A1igator and others added 2 commits March 31, 2026 22:18
…preset

PaymentIndexRecorder deployed on Base Sepolia:
  0x3134920b77565767adf9559E747bED01918B0763

RecorderCombinator runtime codehash:
  0x489c83194f171a41ed97057e542ffb877d7a787f7888341ee379288f4f02691e

Both presets (delivery protection + marketplace) now use
RecorderCombinator([EscrowPeriod, PaymentIndexRecorder]) as
authorizeRecorder when PaymentIndexRecorder is available.

Contracts PR: BackTrackCo/x402r-contracts#32

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both marketplace and delivery protection presets now deploy
RecorderCombinator([EscrowPeriod, PaymentIndexRecorder]) as the
authorizeRecorder when PaymentIndexRecorder is available.

Marketplace deploy function changes:
- Existence check for RecorderCombinator
- trackDeploy for RecorderCombinator
- authorizeRecorderAddress + paymentIndexRecorderAddress in return type
- Early return includes combinator in existing deployments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@A1igator A1igator changed the title feat(core): delivery protection v2 — OrConditions + PaymentIndexRecorder support feat(core): OrConditions + PaymentIndexRecorder for both deploy presets Apr 1, 2026
A1igator and others added 2 commits March 31, 2026 23:19
Reverts marketplace preview/deploy RecorderCombinator additions.
The marketplace preset keeps its existing authorizeRecorder: escrowPeriodAddress.
Marketplace PaymentIndexRecorder support will be a separate PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@A1igator A1igator changed the title feat(core): OrConditions + PaymentIndexRecorder for both deploy presets feat(core): delivery protection v2 — OrConditions + PaymentIndexRecorder Apr 1, 2026
… assertions

PaymentIndexRecorder hasn't been deployed yet — config had a pre-computed
CREATE3 address which caused RecorderCombinator to be deployed referencing
a non-existent contract, breaking authorize flow on fork tests.

- Config: paymentIndexRecorder → zeroAddress (graceful degradation path)
- Unit tests: fix orCondAddr checksum (EIP-55 validation in encodeFunctionData)
- Fork tests: update preview assertions for OrCondition addresses, deploy
  counts from 3 → 5 (escrowPeriod + SAC + 2 OrConditions + operator)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Apr 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.36585% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/core/src/deploy/presets.ts 85.36% 0 Missing and 6 partials ⚠️

📢 Thoughts on this report? Let us know!

A1igator and others added 3 commits March 31, 2026 23:47
Cover the hasCombinator branches in delivery protection preset:
- preview: computeRecorderCombinatorAddress path
- deploy fresh: 6 contracts (includes RecorderCombinator)
- deploy existing: 6 existing when all already deployed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PaymentIndexRecorder is now deployed. Set config address to
0x3134920b77565767adf9559E747bED01918B0763 and update all tests
to expect 6 contracts (with RecorderCombinator) as the default path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PaymentIndexRecorder was deployed at block 39_626_007 on Base Sepolia.
The previous fork block (39_413_000) predated this, causing the
RecorderCombinator to call an empty address during authorize.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vraspar

vraspar commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

SDK Review

Found 7 issues (reviewed: tests, conventions, dead code, SDK design):

  1. [SDK Design] Mutable idx counter for parsing multicall existence results diverges from the named-entries + existsMap pattern already established by deployMarketplaceOperator in the same file. The positional counter with implicit if (hasCombinator) idx++ skip is fragile and order-dependent.

    let idx = 0
    const escrowPeriodExists = existenceResults[idx++].result !== zeroAddress
    const arbiterCondExists = existenceResults[idx++].result !== zeroAddress
    const releaseCondExists = existenceResults[idx++].result !== zeroAddress
    const refundCondExists = existenceResults[idx++].result !== zeroAddress
    const combinatorExists = hasCombinator
    ? existenceResults[idx++].result !== zeroAddress
    : true // no combinator needed
    const operatorExists = existenceResults[idx++].result !== zeroAddress

    Fix: Adopt the same existenceEntries: { name, contract }[] + existsMap approach used by the marketplace preset. The conditional combinator entry is already conditionally spread — idx would not be needed.

  2. [SDK Design] recorderCombinatorCodehash is a bytecode hash (computed via forge script), not a deployment address. Embedding it in X402rChainConfig implies it might differ per chain, which the JSDoc explicitly contradicts ("same for all instances"). Uniswap keeps INIT_CODE_HASH / POOL_INIT_CODE_HASH as module constants separate from chain address registries.

    recorderCombinatorCodehash: Hex

    Fix: Remove recorderCombinatorCodehash from X402rChainConfig and PROTOCOL_ADDRESSES. Keep it as the existing module-level export. Presets can import it directly rather than reading it off getChainConfig().

  3. [Tests] The releaseCondition is now OrCondition([SAC(arbiter), PayerCondition]) — payer is a first-class release path. The fork test only exercises the arbiter leg. There is no fork test for payerClient.payment.release(). The comment at L178 carves out negative condition-enforcement tests for Foundry, but a positive SDK flow test for payer releasing is distinct from that carve-out.

    }, 60_000)
    it('arbiter releases funds (no escrow wait required)', async () => {
    // Arbiter can release immediately — releaseCondition is
    // OrCondition([SAC(arbiter), PayerCondition]), no time delay needed.
    const hash = await arbiterClient.payment.release(
    paymentInfo,
    DEFAULT_AMOUNT,
    )
    await publicClient.waitForTransactionReceipt({ hash })
    const amounts = await merchant.payment.getAmounts(paymentInfo)
    expect(amounts.capturableAmount).toBe(0n)
    }, 60_000)
    })
    // ---------------------------------------------------------------------------
    // Timeout path: authorize → escrow expires → anyone calls refundInEscrow()
    //
    // NOTE: Condition enforcement (non-arbiter cannot release, refundInEscrow
    // reverts during escrow) is tested in Foundry contract tests, not here.
    // The on-chain operator bytecode at the current fork block does not enforce
    // condition checks via revert — no existing fork test uses rejects.toThrow().
    // ---------------------------------------------------------------------------

    Fix: Add a 'payer releases funds' test: authorize a fresh payment, call payerClient.payment.release(), assert capturableAmount === 0n.

  4. [Conventions] deployDeliveryProtectionOperator calls getConditionSingletons(options.chainId) twice inline in the existence-check block (for .payer and .receiver), then declares const singletons again at L1290 for Phase 3. All other resolved values (factoryAddrs, config, authorizedCodehash) are bound at the top of the function.

    abi: orConditionFactoryAbi,
    functionName: 'getDeployed',
    args: [
    [
    arbiterConditionAddress,
    getConditionSingletons(options.chainId).payer,
    ],
    ],
    },
    {
    address: factoryAddrs.orCondition,
    abi: orConditionFactoryAbi,
    functionName: 'getDeployed',
    args: [
    [
    escrowPeriodAddress,
    getConditionSingletons(options.chainId).receiver,
    arbiterConditionAddress,
    ],
    ],
    },

    Fix: Hoist const singletons = getConditionSingletons(options.chainId) to the top alongside factoryAddrs and config. Remove the duplicate declaration at L1290.

  5. [Tests] Fork deploy test 'deploys all components matching preview' only asserts three address fields (operatorAddress, escrowPeriodAddress, arbiterConditionAddress). The three new fields introduced by this PR — releaseConditionAddress, refundInEscrowConditionAddress, authorizeRecorderAddress — are never asserted on the deployment result against real factory bytecode.

    // All deployed addresses should match previewed addresses
    expect(deployment.operatorAddress).toBe(preview.operatorAddress)
    expect(deployment.escrowPeriodAddress).toBe(preview.escrowPeriodAddress)
    expect(deployment.arbiterConditionAddress).toBe(
    preview.arbiterConditionAddress,
    )
    // escrowPeriod + arbiterCondition + 2 OrConditions + recorderCombinator + operator = 6
    expect(deployment.deployments).toHaveLength(6)
    expect(deployment.summary.newCount + deployment.summary.existingCount).toBe(
    6,
    )
    expect(deployment.summary.txHashes).toHaveLength(
    deployment.summary.newCount > 0 ? 1 : 0,
    )

    Fix: Add expect(deployment.releaseConditionAddress).toBe(preview.releaseConditionAddress) and same for refundInEscrowConditionAddress and authorizeRecorderAddress.

  6. [Tests] The fallback test (paymentIndexRecorder = zeroAddress) and the main deploy test both assert result.authorizeRecorderAddress but never assert result.operatorConfig.authorizeRecorder. These are separate fields — operatorConfig is what gets passed to the on-chain factory. A bug that maps the combinator to the wrong OperatorConfig field would pass both tests.

    it('falls back to EscrowPeriod recorder when PaymentIndexRecorder is zeroAddress', async () => {
    const escrowAddr = '0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa' as Address
    const operatorAddr = '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' as Address
    const publicClient = createMockPublicClient({
    computeAddress: escrowAddr,
    [`${F.paymentOperator}:computeAddress`]: operatorAddr,
    })
    const result = await previewDeliveryProtectionOperator(
    publicClient,
    makeDeliveryProtectionOptions({
    paymentIndexRecorderAddress: zeroAddress,
    }),
    )
    // Without PaymentIndexRecorder, authorizeRecorder falls back to escrowPeriod
    expect(result.authorizeRecorderAddress).toBe(escrowAddr)
    expect(result.paymentIndexRecorderAddress).toBe(zeroAddress)
    })

    Fix: Add expect(result.operatorConfig.authorizeRecorder).toBe(escrowAddr) in the fallback test, expect(result.operatorConfig.authorizeRecorder).toBe(combinatorAddr) in the deploy test.

  7. [Tests / Dead Code] The test 'uses provided authorizedCodehash in escrowPeriod computation' was deleted with no replacement. The authorizedCodehash option is still public on DeliveryProtectionOperatorOptions, and the PR changes the default from pad('0x00') to recorderCombinatorCodehash — a behavioral change with zero test coverage.

    Fix: Restore a test that passes a custom authorizedCodehash and asserts a different escrowPeriodAddress, plus a test asserting the new default uses recorderCombinatorCodehash.


Generated with Claude Code using review-sdk skill

A1igator and others added 2 commits April 1, 2026 00:33
Add receipt status check and codehash logging to diagnose
why authorize reverts with the new RecorderCombinator config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Replace mutable idx counter with named entries + existsMap pattern
2. Remove recorderCombinatorCodehash from X402rChainConfig (keep as
   module export — same on every chain, not a per-chain address)
3. Add payer release fork test (OrCondition payer leg)
4. Hoist getConditionSingletons to top of deployDeliveryProtectionOperator
5. Add missing address assertions in fork deploy test
6. Add operatorConfig.authorizeRecorder assertions in unit tests
7. Add authorizedCodehash default + override test coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@A1igator

A1igator commented Apr 1, 2026

Copy link
Copy Markdown
Contributor Author

All 7 fixed in a9fdbec:

  1. idx counter → named entries + existsMap (matches marketplace preset)
  2. recorderCombinatorCodehash in chain config → removed from X402rChainConfig + PROTOCOL_ADDRESSES, kept as module export
  3. payer release fork test → added with salt 201n
  4. duplicate getConditionSingletons → hoisted to top of function
  5. missing fork deploy assertions → added releaseConditionAddress, refundInEscrowConditionAddress, authorizeRecorderAddress
  6. operatorConfig.authorizeRecorder assertions → added in both 6-contract and 5-contract tests
  7. authorizedCodehash tests → added default + custom override coverage

@vraspar
vraspar self-requested a review April 1, 2026 07:50
Diagnostics revealed the codehash mismatch causing authorize reverts:
- Config had 0x489c... (from local forge script against updated source)
- On-chain factory deploys instances with codehash 0xeb39...

The factory was deployed with an older RecorderCombinator version.
Updated config to the on-chain value verified via EXTCODEHASH.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vraspar

vraspar commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Protocol Analysis: Delivery Protection v2

Deep review of the condition matrix changes and protocol implications.


Condition Matrix (v1 → v2)

Slot v1 v2
releaseCondition SAC(arbiter) OrCondition([SAC(arbiter), Payer])
refundInEscrowCondition EscrowPeriod OrCondition([EscrowPeriod, Receiver, SAC(arbiter)])
authorizeRecorder EscrowPeriod RecorderCombinator([EscrowPeriod, PaymentIndexRecorder])
refundPostEscrowCondition Receiver Receiver (unchanged)

What works well

  • Arbiter immediate refund (FAIL-fast) — Core win. In v1, if the arbiter detects garbage content, the payer waits the full escrow period for timeout-refund. Now the arbiter refunds instantly. For HTTP 402 micropayments where quality is checked in seconds, this is a significant UX improvement.
  • Receiver voluntary refund — Honest merchant can proactively refund without waiting for arbiter or escrow expiry.
  • RecorderCombinator — Clean composition pattern. On-chain payment indexing without external indexer dependency.

Concerns to discuss

1. Arbiter trust surface doubled

v1 v2
Arbiter can release (funds → receiver)
Arbiter can refund (funds → payer)

A compromised/buggy arbiter now controls fund movement in both directions. A rogue arbiter could systematically refund all payments (denying receiver revenue) or release all payments (denying payer refunds). Probably acceptable for the EigenCloud verifiable arbiter model, but this is a trust assumption change worth documenting.

2. Payer self-release partially undermines the protection guarantee

v1: arbiter is the sole quality gate — every payment goes through quality inspection.
v2: payer can release immediately, arbiter never checks.

In the HTTP 402 flow where the payer is automated, client-side code could skip quality verification entirely. The "delivery protection" becomes opt-in from the payer's perspective rather than enforced. Is this intentional?

3. charge slots remain zeroAddress

Both chargeCondition and chargeRecorder are zeroAddress. If someone calls operator.charge(), it bypasses all conditions and RecorderCombinator — the payment won't be indexed by PaymentIndexRecorder. Pre-existing gap from v1, but more visible now that indexing is a feature. Consider whether charge should be explicitly disabled for this preset.


Missing fork test coverage

The 3 most important new paths have no fork tests:

New behavior Fork test?
Payer self-release ✅ Added (salt 201)
Arbiter immediate refund (FAIL-fast) ❌ Missing
Receiver voluntary refund during escrow ❌ Missing
PaymentIndexRecorder data verification ❌ Missing

The arbiter-immediate-refund is the core motivation for this PR and has no fork test. Recommended additions:

  • arbiterClient.payment.refundInEscrow() — without time-forwarding, should succeed immediately
  • receiverClient.payment.refundInEscrow() — without time-forwarding, should succeed immediately
  • After authorize, query PaymentIndexRecorder.getPayerPayments(payer, 0, 10) to verify on-chain indexing works end-to-end

Generated with Claude Code using review-sdk skill

…IndexRecorder

- Arbiter immediate refund: refundInEscrow without time-forwarding
- Receiver voluntary refund: merchant refunds during escrow
- PaymentIndexRecorder: verify on-chain payment indexing via getPayerPayments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@A1igator

A1igator commented Apr 1, 2026

Copy link
Copy Markdown
Contributor Author

Protocol Analysis Response

Concerns

1. Arbiter trust surface doubled — The escrow period for delivery protection is 2 minutes. A rogue arbiter refunding during that window is operationally equivalent to the escrow expiring — the payer gets funds back either way. The arbiter shortcut just removes the 2-minute delay for legitimate FAIL verdicts. In practice the arbiter verdict lands in <5s, well before any receiver could capture. The EigenCloud arbiter is verifiable compute (deterministic + attestable) so this is a low-risk tradeoff for a significant UX win.

2. Payer self-release — Intentional. The HTTP 402 flow has two actors: automated client and automated arbiter. Payer self-release handles the case where the payer inspects content client-side and is satisfied before the arbiter round-trip completes. The protection guarantee is that someone (arbiter or payer) validates quality, not that the arbiter is the sole gate.

3. charge slots as zeroAddress — Pre-existing from v1. Delivery protection doesn't use the charge flow (authorize → release/refund, no partial captures). Not worth a RevertCondition for a flow that's never called.

Missing fork tests

Added in 77d00a0:

  • Arbiter immediate refundarbiterClient.payment.refundInEscrow() without time-forwarding (salt 203)
  • Receiver voluntary refundmerchant.payment.refundInEscrow() during escrow (salt 204)
  • PaymentIndexRecorder verification — queries getPayerPayments() and asserts indexed count > 0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@A1igator
A1igator force-pushed the A1igator/delivery-protection-v2 branch from 049cf22 to 9ee6003 Compare April 1, 2026 08:22
A1igator and others added 4 commits April 1, 2026 01:37
PaymentIndexRecorder redeployed at 0xA9D08f46... with correct
authorizedCodehash (0xeb3902c8...). Previous instance at 0x3134920b...
was deployed with stale codehash (0x489c8319...) that didn't match
the on-chain RecorderCombinator runtime codehash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New PaymentIndexRecorder at 0xA9D08f46... deployed at block 39_632_073.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ploy

Redeployed PaymentIndexRecorder with correct IRecorder interface (4-param
record() with bytes calldata data). Previous deploys were compiled from a
branch missing the data passthrough commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vraspar

vraspar commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Configurability gap

The OrCondition compositions are hardcoded — deployers cannot opt out of individual legs:

Behavior Configurable?
PaymentIndexRecorder paymentIndexRecorderAddress: zeroAddress to skip
Payer self-release ❌ Hardcoded in release OrCondition
Receiver voluntary refund ❌ Hardcoded in refundInEscrow OrCondition
Arbiter immediate refund ❌ Hardcoded in refundInEscrow OrCondition

A deployer who wants arbiter-only release (v1 behavior) or doesn't want receiver refund during escrow has no option — they'd have to skip the preset entirely.

Suggestion: Add optional flags to DeliveryProtectionOperatorOptions:

/** Allow payer to release without arbiter (default: true) */
allowPayerRelease?: boolean
/** Allow receiver to refund during escrow (default: true) */
allowReceiverRefund?: boolean
/** Allow arbiter to refund immediately during escrow (default: true) */
allowArbiterRefund?: boolean

When false, that leg is excluded from the OrCondition (or the OrCondition is skipped entirely and the single condition is used directly — matching v1 behavior). Low cost to add, gives deployers control over their trust model.


Generated with Claude Code using review-sdk skill

Only arbiter immediate refund is configurable — payer self-release and
receiver voluntary refund are core to the delivery protection model.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@A1igator

A1igator commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

Added allowArbiterRefund in ab188ec — only that leg is configurable.

Payer self-release and receiver voluntary refund are not optional — they're core to the delivery protection model:

  • Payer self-release: if the payer verifies content client-side before the arbiter round-trip, they should be able to release immediately. Removing this would force unnecessary arbiter latency on satisfied payers.
  • Receiver voluntary refund: honest merchants must always be able to proactively refund. There's no trust model where removing this makes sense — it only hurts the receiver's ability to act in good faith.

allowArbiterRefund (default: true) is the one that's genuinely situational — some deployers may want the v1 behavior where refund requires escrow expiry, especially if they don't trust the arbiter to refund correctly. When false, the refundInEscrow OrCondition drops from 3 legs to 2: [EscrowPeriod, ReceiverCondition].

@A1igator
A1igator merged commit 0be1473 into main Apr 2, 2026
9 checks passed
@A1igator
A1igator deleted the A1igator/delivery-protection-v2 branch April 2, 2026 02:18
@A1igator

A1igator commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: flipped allowArbiterRefund default to false (opt-in) in #107. Arbiter refund power should be a conscious choice, not implicit.

A1igator added a commit that referenced this pull request Apr 2, 2026
## Summary

- Flips `allowArbiterRefund` default from `true` to `false` — arbiter
immediate refund is now opt-in
- Deployers must explicitly pass `allowArbiterRefund: true` to grant
arbiter refund power during escrow
- Makes the trust surface explicit rather than implicit

Follows up on #105 review feedback (configurability gap).

## Test plan

- [x] 341 unit tests passing
- [x] Typecheck passes
- [x] Biome check passes
- [x] Fork test updated to explicitly opt in (`allowArbiterRefund:
true`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

2 participants