diff --git a/docs/project-status.md b/docs/project-status.md index c609a9a..120a569 100644 --- a/docs/project-status.md +++ b/docs/project-status.md @@ -2,9 +2,9 @@ **Last Updated:** September 7, 2026 **Project Phase:** Solidity / Reference Implementation -**Current Implementation Slice:** F8C — Authoritative Settlement / Direct Beneficiary Delivery (NEXT AUTHORIZED / NOT STARTED) -**Last Closed Gate:** G8B — O2 Exact-Output Execution / Execution-Evidence Gate (CLOSED / PASS) -**Status:** F0 COMPLETE — F1 COMPLETE — F2 COMPLETE — F3 COMPLETE — F4 COMPLETE — F5 COMPLETE — F6A COMPLETE — F7 COMPLETE — F6B COMPLETE — F8A COMPLETE — F8B COMPLETE — F8C NOT STARTED +**Current Implementation Slice:** F8D — O2 Causal Finalization / Remaining Entitlement Reduction (NEXT AUTHORIZED / NOT STARTED) +**Last Closed Gate:** G8C — Authoritative Settlement / Delivery Gate (CLOSED / PASS) +**Status:** F0 COMPLETE — F1 COMPLETE — F2 COMPLETE — F3 COMPLETE — F4 COMPLETE — F5 COMPLETE — F6A COMPLETE — F7 COMPLETE — F6B COMPLETE — F8A COMPLETE — F8B COMPLETE — F8C COMPLETE — F8D NOT STARTED --- @@ -41,15 +41,15 @@ Implementation proceeds through the verification-gated F0–F10 ladder defined i The next authorized slice is: -> **F8C — Authoritative Settlement / Direct Beneficiary Delivery** +> **F8D — O2 Causal Finalization / Remaining Entitlement Reduction** The last closed gate is: -> **G8B — O2 Exact-Output Execution / Execution-Evidence Gate** +> **G8C — Authoritative Settlement / Delivery Gate** -F0, F1, F2, F3, F4, F5, F6A, F7, F6B, F8A and F8B have been implemented, verified, and explicitly gate-closed. The targeted G3 revalidation required by the F5 prospective-derivability correction also passed, and G3 remains closed. +F0, F1, F2, F3, F4, F5, F6A, F7, F6B, F8A, F8B and F8C have been implemented, verified, and explicitly gate-closed. The targeted G3 revalidation required by the F5 prospective-derivability correction also passed, and G3 remains closed. -F8B was reviewed independently and G8B was explicitly closed. F8C is the next authorized implementation slice and has not been started. +F8C was reviewed independently and G8C was explicitly closed. F8D is the next authorized implementation slice and has not been started. --- @@ -70,8 +70,8 @@ Current implementation sequence: | F6B | O3 Enforcement with Authentic O > 0 | **COMPLETE — G6B CLOSED** | | F8A | O2 Authorization | **COMPLETE — G8A CLOSED** | | F8B | O2 Exact-Output Execution + Causal Evidence | **COMPLETE — G8B CLOSED** | -| F8C | O2 Input Settlement + Direct Beneficiary Delivery | **NEXT / NOT STARTED** | -| F8D | O2 Causal Finalization | NOT STARTED | +| F8C | O2 Input Settlement + Direct Beneficiary Delivery | **COMPLETE — G8C CLOSED** | +| F8D | O2 Causal Finalization | **NEXT / NOT STARTED** | | GI | Full Stateful Invariant Verification | NOT STARTED | | F9 | Canonical Acceptance | NOT STARTED | | F10 | Demo Instrumentation | NOT STARTED | @@ -217,8 +217,8 @@ inert completion plumbing. F7 adds the production O1 commitment-admission transition. F8A adds the production O2 authorization transition and its Hook-owned transaction-scoped causal context. -F8B adds the production O2 execution classification and execution evidence. Settlement, delivery and -finalization remain unimplemented. +F8B adds the production O2 execution classification and execution evidence. F8C adds the production O2 +input settlement and direct Beneficiary delivery. Finalization remains unimplemented. ### `script/DeployStandbyHook.s.sol` @@ -371,14 +371,14 @@ G2 verified predicate independence, administrator authority, read fidelity, cros ## 10. Current Blocker -There is no unresolved F0, F1, F2, F3, F4, F5, F6A, F7, F6B, F8A or F8B implementation responsibility and no open gate. +There is no unresolved F0, F1, F2, F3, F4, F5, F6A, F7, F6B, F8A, F8B or F8C implementation responsibility and no open gate. The next unstarted responsibility is: -> **F8C — Authoritative Settlement / Direct Beneficiary Delivery.** +> **F8D — O2 Causal Finalization / Remaining Entitlement Reduction.** -F8C is authorized as the next implementation slice but has not been started. No further downstream slice — -F8D — is authorized. +F8D is authorized as the next implementation slice but has not been started. No further downstream slice — +GI — is authorized. Known limitations carried forward from F0, none of which blocked G0: @@ -389,8 +389,7 @@ Known limitations carried forward from F0, none of which blocked G0: node with `--broadcast`; - the four enabled Hook callbacks now enforce ordinary O3 admission, and refuse everything while no Protected Execution Service is configured. `StandbyHook` must still not be attached to a live pool - before its service is activated, and O2 settlement, delivery and finalization remain - unimplemented. + before its service is activated, and O2 finalization remains unimplemented. --- @@ -582,11 +581,11 @@ The root `CLAUDE.md` will define the repository operating rules and document aut ## 18. Next Action -G8B has been reviewed and closed, so the next action is: +G8C has been reviewed and closed, so the next action is: -> **F8C — Authoritative Settlement / Direct Beneficiary Delivery.** +> **F8D — O2 Causal Finalization / Remaining Entitlement Reduction.** -F8C may begin when explicitly tasked. Its own verification gate governs advancement beyond the F8C boundary. +F8D may begin when explicitly tasked. Its own verification gate governs advancement beyond the F8D boundary. --- @@ -607,16 +606,17 @@ F8C may begin when explicitly tasked. Its own verification gate governs advancem - F6B O3 Enforcement with Authentic O > 0 is complete and G6B is closed. - F8A O2 Authorization / Hook-Owned Causal Context is complete and G8A is closed. - F8B O2 Exact-Output Execution / Execution Evidence is complete and G8B is closed. -- No O2 settlement, delivery or finalization behavior has been introduced. +- F8C O2 Input Settlement / Direct Beneficiary Delivery is complete and G8C is closed. +- No O2 finalization behavior has been introduced. **Current Gate** -- G8B — CLOSED / PASS. No gate is currently open. +- G8C — CLOSED / PASS. No gate is currently open. **Next Blocker** -- F8C — Authoritative Settlement / Direct Beneficiary Delivery has not been started. +- F8D — O2 Causal Finalization / Remaining Entitlement Reduction has not been started. **Immediate Next Step** -- Begin F8C when explicitly tasked. +- Begin F8D when explicitly tasked. diff --git a/docs/prompts/retrospective/session-13-chatgpt-record.md b/docs/prompts/retrospective/session-13-chatgpt-record.md new file mode 100644 index 0000000..5d3b1e3 --- /dev/null +++ b/docs/prompts/retrospective/session-13-chatgpt-record.md @@ -0,0 +1,988 @@ +# Session 13 — ChatGPT Reasoning Record + +**Implementation Slice:** F8C — Authoritative Settlement / Direct Beneficiary Delivery +**Gate:** G8C +**Final Gate Determination:** PASS / CLOSED +**Record Type:** Non-normative curated ChatGPT reasoning record +**Purpose:** Post-project methodology retrospective evidence + +--- + +## 1. Record Purpose + +This document preserves the substantive user ↔ ChatGPT reasoning associated with F8C. + +It is intentionally **non-normative**. It does not define Standby protocol semantics, supersede frozen canonical artifacts, or replace the implementation plan, realization artifact, project status, production code, tests, or Claude implementation log. + +Its purpose is to preserve contemporaneous evidence for the post-ETHGlobal retrospective, including: + +- user questions, challenges, concerns, and proposed decisions; +- ChatGPT derivations and recommendations; +- responsibility-boundary decisions; +- material design alternatives; +- derivation of G8C; +- preparation and refinement of the Claude implementation prompt; +- independent review of Claude's implementation and tests; +- gate-closure reasoning; +- methodology observations arising during the slice. + +Where this record summarizes rather than reproduces exact conversation wording, it should be treated as a **recovered decision record**, not a verbatim transcript. + +--- + +## 2. Starting State + +Session 13 began after completion and independent closure of: + +- F0 — Foundation; +- F1 — Deterministic Economic Fixture; +- F2 — Eligibility Registry; +- F3 — StandbyHook Trust + PES Configuration; +- F4 — Commitment Storage / Bounded References; +- F5 — Authoritative Derivation Kernel; +- F6A — Preliminary O3 Enforcement; +- F7 — O1 Commitment Admission; +- F6B — O3 Bounded Enforcement; +- F8A — Exercise Authorization; +- F8B — Protected PoolManager Execution. + +The implementation frontier was therefore: + +> **F8C — Authoritative Settlement / Direct Beneficiary Delivery** + +F8B had already established the protected PoolManager execution responsibility: + +1. an F8A-authorized exercise existed; +2. the exact protected PoolManager operation was derived from Hook-owned causal context; +3. the operation executed through the configured ExerciseRouter; +4. the Hook verified the actual protected output from the authoritative PoolManager `BalanceDelta`; +5. the Hook causal context advanced to `EXECUTED`. + +F8B deliberately did **not** establish: + +- settlement of the input debt; +- enforcement of the exerciser's `maxInput`; +- payment by the authenticated exerciser; +- delivery to the authoritative Beneficiary; +- fulfillment; +- Remaining Entitlement reduction; +- final backing verification; +- causal-context consumption. + +Those unresolved responsibilities formed the starting boundary for F8C. + +--- + +## 3. Working Responsibility Decomposition + +The ExerciseRouter path remained decomposed as: + +- **R1 + R2 → F8A:** authorization and authoritative exercise identity; +- **R3 → F8B:** exact protected PoolManager execution; +- **R4 → F8C:** authoritative settlement and direct Beneficiary delivery; +- **R5 → F8D:** final backing, fulfillment consequence, Remaining Entitlement reduction, and causal-context consumption. + +A central Session 13 objective was to ensure that implementing R4 did not absorb any R5 responsibility. + +The resulting F8C responsibility was derived as: + +> After the one F8B-proven protected exact-output PoolManager execution reaches Hook-owned `EXECUTED`, F8C consumes the authoritative input-side PoolManager debt produced by that execution, enforces the originating exerciser's `maxInput` against that actual debt, settles exactly that debt directly from the authenticated exerciser to PoolManager, and discharges the corresponding protected-output PoolManager credit by transferring exactly `q` directly from PoolManager to the authoritative Beneficiary. + +The following must remain unchanged: + +- Original Entitlement; +- Remaining Entitlement; +- obligation consequences; +- fulfillment status; +- commitment completion; +- Hook causal context, which remains `EXECUTED`. + +This became the principal F8C responsibility boundary. + +--- + +## 4. Authoritative Actual-Input Derivation + +A major derivation concerned the source of settlement truth. + +The conclusion was that F8C must not settle using: + +- a quote; +- requested `maxInput`; +- requested output `q`; +- an independently recomputed estimate; +- Router-held accounting; +- a pre-swap prediction. + +The authoritative input debt already exists in the exact `BalanceDelta` returned by the F8B PoolManager execution. + +For the protected exact-output operation: + +### zeroForOne + +- input currency = `currency0`; +- output currency = `currency1`; +- authoritative input debt = negative `amount0`; +- authoritative output evidence = positive `amount1 == q`. + +### oneForZero + +- input currency = `currency1`; +- output currency = `currency0`; +- authoritative input debt = negative `amount1`; +- authoritative output evidence = positive `amount0 == q`. + +The input side must therefore be negative. + +Zero or positive values on the expected input side are not alternative representations of valid debt; they are invalid authoritative execution evidence and must fail closed. + +The derivation explicitly rejected use of absolute value. + +--- + +## 5. Signed-Integer Safety + +The signed PoolManager delta introduced an important implementation detail. + +Because the PoolManager delta component is `int128`, directly negating the `int128` minimum value could overflow before conversion. + +The accepted conversion shape was therefore: + +```solidity +int256 inputDelta = int256(inputSideDelta); + +if (inputDelta >= 0) revert ...; + +uint256 actualInput = uint256(-inputDelta); +``` + +The important rule was: + +> **Widen before negation.** + +This became an explicit G8C requirement and later received boundary and fuzz coverage. + +--- + +## 6. Reuse of the Exact R3 Execution Object + +An implementation-design question arose over whether F8C should reconstruct the executed operation from Hook configuration after R3. + +That was rejected. + +The preferred design was to have the F8B execution helper carry forward the exact transaction-local execution facts: + +- `PoolKey`; +- `SwapParams`; +- `BalanceDelta`. + +This avoided creating a second description of the protected operation between R3 and R4. + +The reasoning was: + +> If R4 depends on facts produced by R3, the strongest causal continuity is obtained by consuming the exact R3 execution object rather than independently reconstructing an equivalent operation. + +Claude subsequently implemented this by changing `_executeAuthorizedExercise()` to return the exact execution facts. + +--- + +## 7. `maxInput` Responsibility + +`maxInput` was classified as **request-local exerciser cost protection**. + +It is not: + +- Hook economic state; +- commitment state; +- fulfillment state; +- a persistent authorization fact; +- part of Supporting Capacity or Obligation; +- part of the causal context. + +The required condition was: + +```text +actualInput <= maxInput +``` + +Therefore: + +- `actualInput < maxInput` passes; +- `actualInput == maxInput` passes; +- `actualInput > maxInput` reverts the entire exercise. + +The comparison must occur against the actual PoolManager debt, before settlement or delivery effects are allowed to commit. + +Transporting `maxInput` through the PoolManager unlock payload was accepted because it remains transaction-local and does not contaminate Hook-owned authoritative state. + +--- + +## 8. Authoritative Payer + +A central responsibility question was: **who economically funds the protected execution?** + +The answer was the F8A-authenticated originating exerciser already bound into the Hook-owned causal context. + +The payer must not become: + +- the ExerciseRouter; +- the PoolManager swap caller merely because it owns the PoolManager delta; +- the Beneficiary; +- `tx.origin`; +- a test harness; +- a pre-funded reserve; +- an arbitrary approved third party. + +The accepted economic path was: + +```text +authenticated exerciser + ↓ + transferFrom + ↓ + PoolManager +``` + +The Router coordinates the transfer but does not become the economic principal. + +This distinction was especially important because the pre-existing F8B `ExerciseDeltaClosureRouter` deliberately used Router pre-funding to close PoolManager deltas. That behavior was valid as isolated F8B mechanical evidence but explicitly invalid as positive F8C payer semantics. + +--- + +## 9. Settlement Sequence + +The accepted ERC-20 settlement sequence was: + +```text +derive authoritative actualInput + ↓ +enforce actualInput <= maxInput + ↓ +PoolManager.sync(inputCurrency) + ↓ +transfer exactly actualInput +from authenticated exerciser +directly to PoolManager + ↓ +PoolManager.settle() + ↓ +prove Router's input-side PoolManager delta == 0 +``` + +A call to `settle()` alone was considered insufficient evidence. + +F8C needed an explicit postcondition proving that the relevant input debt created by the protected execution had actually been discharged. + +--- + +## 10. Direct Beneficiary Delivery + +The protected output already exists as a PoolManager credit following F8B. + +F8C therefore should not route that value through the ExerciseRouter or Hook. + +The accepted delivery was conceptually: + +```solidity +PoolManager.take( + outputCurrency, + authoritativeBeneficiary, + q +); +``` + +The Beneficiary must come from the Hook-owned causal context established by F8A. + +The recipient must not be: + +- caller supplied; +- Router selected; +- inferred from `msg.sender`; +- substituted by the exerciser; +- substituted by a harness. + +The intended value path was: + +```text +PoolManager + ↓ +authoritative Beneficiary +``` + +not: + +```text +PoolManager → Router → Beneficiary +``` + +and not: + +```text +PoolManager → Hook → Beneficiary +``` + +The corresponding output-side PoolManager credit must then be proven zero. + +--- + +## 11. No New F8C Lifecycle State + +A possible design direction would have been to add states such as: + +- `SETTLED`; +- `DELIVERED`; + +or persistent facts such as: + +- `actualInput`; +- settlement nonce; +- delivery nonce; +- payer snapshot; +- delivery flag. + +That was rejected. + +The existing Hook-owned causal context already identifies the one exercise transaction and the authoritative facts required by F8C. + +F8C therefore leaves the state: + +```text +EXECUTED +``` + +after settlement and delivery. + +This preserved Semantic Minimality and avoided creating intermediate states whose only purpose would be to bridge transaction-local operations. + +--- + +## 12. Discovery of the Production Completion Hazard + +The most consequential derivation of Session 13 emerged from considering what would happen if F8C successfully returned before F8D existed. + +At F8C: + +- the protected swap could have executed; +- the exerciser could have paid; +- the Beneficiary could have received `q`; +- Remaining Entitlement would still be unchanged; +- the Hook causal context would still be `EXECUTED`. + +Because the causal context is transaction-scoped, successful completion of that top-level transaction would discard the context. + +A later transaction could therefore potentially exercise the same still-unreduced entitlement again. + +The dangerous incomplete sequence was: + +```text +protected execution + ↓ +settlement + ↓ +Beneficiary receives q + ↓ +Remaining unchanged + ↓ +transaction completes + ↓ +transient causal proof disappears +``` + +This violated the already-frozen atomicity requirement that no durable state may exist where delivery survives but Standby finalization fails. + +--- + +## 13. F8C Production Completion Barrier + +The solution was not to pull F8D into F8C. + +Instead, Session 13 derived a **slice-boundary completion barrier**: + +> Until F8D exists, a production F8C-only top-level `exercise()` must not successfully commit settlement and Beneficiary delivery while the Hook remains `EXECUTED`. + +The production exercise path therefore needs to require that the Hook causal context has been consumed before the top-level exercise can return successfully. + +At F8C: + +```text +R4 completes +→ context remains EXECUTED +→ completion barrier fails +→ entire transaction reverts +``` + +After F8D: + +```text +R4 completes +→ F8D performs final backing and fulfillment +→ Remaining is reduced +→ context is consumed / EMPTY +→ existing completion barrier succeeds +``` + +This was a significant architectural result because the barrier is not temporary scaffolding that must later be deleted. It is a forward-compatible production postcondition. + +The decision preserved the F8D ownership boundary while preventing an incomplete F8C implementation from creating durable economic state. + +--- + +## 14. Testability Without Fake Finalization + +The completion barrier creates an implementation-stage testing problem: + +If every production F8C transaction must revert until F8D exists, how can the actual R4 mechanics be positively observed? + +The accepted solution was a **narrow test-only harness** that bypasses only the final top-level completion requirement. + +The harness was permitted only if it continued to use production: + +- F8A authorization; +- F8B execution; +- authoritative debt derivation; +- `maxInput`; +- payer attribution; +- settlement; +- Beneficiary resolution; +- delivery; +- PoolManager delta closure. + +It must not: + +- consume the Hook context; +- reduce Remaining; +- invent fulfillment; +- redirect the payer; +- redirect output; +- redefine settlement; +- create pseudo-F8D behavior. + +Claude implemented `UnfinalizedExerciseRouter` accordingly. + +Independent review later confirmed that its only production-path semantic bypass was the completion-barrier check. + +--- + +## 15. Existing F8B Harness Boundary + +The pre-existing `ExerciseDeltaClosureRouter` mechanically: + +- used pre-funded Router balances for settlement; +- took protected output to the Router. + +That was appropriate for F8B's narrow question: + +> Can the exact protected PoolManager execution occur and have its PoolManager deltas mechanically closed in the test environment? + +It was not appropriate for F8C's economic questions: + +- Who pays? +- How is `maxInput` enforced? +- Who receives protected output? + +Session 13 explicitly preserved that harness as isolated F8B evidence rather than repurposing it as positive F8C evidence. + +This prevented earlier test infrastructure from silently becoming later protocol semantics. + +--- + +## 16. G8C Derivation + +G8C was derived before Claude implementation as a 19-condition gate. + +The gate required proof of: + +1. authoritative currency identity in both directions; +2. actual input derived from the negative input-side PoolManager `BalanceDelta`; +3. correct signed interpretation and rejection of invalid sign; +4. safe signed conversion; +5. exact `maxInput` boundary; +6. request-local `maxInput`; +7. authoritative payer identity; +8. exact direct exerciser-to-PoolManager funding; +9. pinned-v4 settlement and explicit input-delta closure; +10. rejection of under/over settlement; +11. authoritative Beneficiary identity; +12. direct PoolManager-to-Beneficiary delivery of exactly `q`; +13. no Router/Hook output custody and output-delta closure; +14. `EXECUTED` causal prerequisite; +15. one R4 sequence without split/netted/unrelated activity; +16. no F8D responsibility leakage; +17. no new causal lifecycle state; +18. full failure atomicity; +19. production slice-boundary completion safety. + +G8C-19 was added because of the production-completion hazard discovered during derivation rather than during implementation. + +This is important retrospective evidence: the pre-implementation reasoning phase identified an economic atomicity defect that would otherwise have existed in a locally correct F8C implementation. + +--- + +## 17. Claude Prompt Ownership Refinement + +During preparation of the final Claude prompt, the session reaffirmed the clean prompt-ownership model: + +> **`CLAUDE.md` owns permanent operating behavior.** +> **`.claude/rules/*` owns permanent Solidity/testing conventions.** +> **Session prompts own only slice-specific objective, scope, requirements, prohibitions, file boundaries, gate evidence, and completion boundary.** + +The F8C prompt was reviewed for leakage against that model. + +General process language such as ChatGPT's responsibility to independently close G8C was removed from the implementation prompt. + +The final prompt retained: + +- F8C-specific requirements; +- G8C evidence requirements; +- production completion barrier; +- explicit F8D prohibition; +- slice-specific verification commands; +- blocker handling limited to F8C ambiguity. + +This maintained the distinction between permanent agent behavior and slice-local implementation responsibility. + +--- + +## 18. Claude Implementation Result + +Claude reported implementing F8C primarily in: + +```text +src/ExerciseRouter.sol +``` + +with supporting test infrastructure including: + +```text +test/harness/UnfinalizedExerciseRouter.sol +test/harness/ExerciseDeltaClosureRouter.sol +test/shared/BaseExerciseSettlementTest.t.sol +test/unit/ExerciseSettlementDerivation.t.sol +test/integration/ExerciseSettlement.t.sol +test/fuzz/ExerciseSettlementFuzz.t.sol +test/periphery/ExerciseSettlementPerimeter.t.sol +``` + +Claude reported: + +```text +502 passed +0 failed +0 skipped +54 suites +``` + +and CI-profile execution at 10,000 fuzz runs. + +Claude proposed G8C PASS, but that proposal was treated as advisory only. + +No gate was closed from the implementation-agent report alone. + +--- + +## 19. Independent Review Method + +Session 13 introduced an improvement to the independent-review workflow. + +Instead of reviewing only separately uploaded final source files or relying on Claude's implementation summary, the user generated and supplied a complete Git diff between the previously gated repository state and the F8C implementation branch. + +The independent review therefore examined the **exact version-controlled implementation delta**. + +The diff included: + +- production changes; +- harness changes; +- shared test infrastructure; +- unit tests; +- integration tests; +- fuzz tests; +- periphery tests; +- session-log changes. + +The temporary diff file was used as review input only and was intentionally not preserved as a repository artifact. + +Git history and the eventual PR remain the durable source of the implementation delta. + +This review method made it easier to evaluate: + +- whether production scope remained bounded; +- whether previously gated F8A/F8B behavior was altered; +- whether test harnesses substituted for production semantics; +- whether tests were newly added, weakened, or removed; +- whether Claude's completion report accurately described the actual implementation. + +--- + +## 20. Independent Production Review + +The production review confirmed the intended causal ordering: + +```text +authorize + ↓ +PoolManager unlock + ↓ +exact protected swap + ↓ +Hook reaches EXECUTED + ↓ +derive actual PoolManager input debt + ↓ +maxInput check + ↓ +authenticated exerciser funds PoolManager + ↓ +settle + ↓ +input delta == 0 + ↓ +PoolManager takes q directly to Beneficiary + ↓ +output delta == 0 + ↓ +context remains EXECUTED +``` + +Only after the unlock returns does the production completion barrier require the exercise to have been finalized. + +At F8C that requirement fails because the context remains `EXECUTED`, causing the complete transaction to revert. + +No production responsibility leak into F8D was found. + +--- + +## 21. Independent Harness Review + +Particular scrutiny was applied to `UnfinalizedExerciseRouter`. + +The review confirmed that it inherits the production: + +- authorization path; +- originating-exerciser attribution; +- PoolManager callback authentication; +- R3 protected execution; +- actual-input derivation; +- `maxInput` enforcement; +- payer semantics; +- settlement mechanics; +- Beneficiary resolution; +- delivery mechanics; +- delta-closure checks. + +Its relevant bypass is limited to the production finalization requirement. + +This was considered an appropriate test seam because it exposes the F8C R4 effects without redefining them. + +The existing F8B `ExerciseDeltaClosureRouter` remained isolated as F8B evidence and was not used as the positive F8C economic path. + +--- + +## 22. G8C-9 and G8C-10 Scrutiny + +Two gate conditions received additional review because Claude described their direct adversarial reachability as limited. + +### G8C-9 — Input Delta Closure + +Independent inspection confirmed that production explicitly checks the Router's relevant input-side PoolManager `currencyDelta` after settlement. + +Therefore settlement correctness does not rely solely on PoolManager's eventual end-of-unlock open-delta enforcement. + +G8C-9 was considered directly structurally proven. + +### G8C-10 — No Under/Over Settlement + +Claude noted that deliberately under- or over-settling is unreachable through the supported production path. + +Independent review agreed. + +Production derives one authoritative value: + +```text +actualInput +``` + +from the PoolManager execution result and passes that exact value to the direct `transferFrom`. + +After `settle()`, the corresponding PoolManager delta must equal zero. + +Therefore: + +```text +authoritative debt + ↓ +same exact amount transferred + ↓ +settle + ↓ +authoritative delta == 0 +``` + +Underpayment would leave negative debt. + +Overpayment would leave positive credit. + +Either fails the zero-delta postcondition. + +Creating a test that deliberately transfers a different amount would require overriding or altering the production settlement mechanic. Such a test would primarily prove behavior of an intentionally malformed test harness rather than demonstrate a reachable production path. + +The conclusion was: + +> **G8C-10 correctness risk is effectively closed; direct adversarial-test coverage is somewhat weaker than for some other gate conditions, but structural unreachability plus the authoritative downstream zero-delta postcondition is sufficient.** + +No corrective implementation was requested. + +--- + +## 23. Direction-General Evidence + +The canonical real-stack fixture exercises the protected `zeroForOne` direction. + +Claude correctly recorded that a mirrored real-stack `oneForZero` fixture was not introduced in F8C. + +Independent review did not treat this as a blocker because the direction-sensitive production logic is localized to: + +- selection of input currency; +- selection of output currency; +- selection of the authoritative input-side `BalanceDelta`. + +That exact production derivation function is unit-tested and fuzzed across both directions and signed-delta boundaries. + +The downstream settlement and delivery helpers consume the already-derived currencies and are direction-agnostic. + +The absence of a second mirrored integration fixture was therefore classified as a documented test-fixture reachability limitation rather than an unproven F8C semantic branch. + +--- + +## 24. Failure Atomicity Review + +The independent review confirmed coverage for reachable failure classes including: + +- `actualInput > maxInput`; +- insufficient exerciser balance; +- insufficient exerciser allowance; +- invalid causal state; +- direct callback attempts; +- production completion before F8D. + +Atomicity assertions included more than token balances. + +Tests compared relevant: + +- pool price; +- tick; +- liquidity; +- PoolManager balances; +- Router balances; +- Hook balances; +- Beneficiary balance; +- Original Entitlement; +- Remaining Entitlement; +- aggregate obligation; +- causal context. + +No relevant production exception was caught and converted into partial success. + +Failures therefore propagate through the unlock transaction and revert the preceding protected execution, settlement, and delivery. + +--- + +## 25. Final G8C Determination + +After independent review of the actual implementation delta and tests: + +> **F8C — Authoritative Settlement / Direct Beneficiary Delivery: COMPLETE** + +> **G8C: PASS / CLOSED** + +All 19 gate conditions were considered adequately discharged. + +No corrective Claude implementation cycle was required. + +The final F8C causal state is: + +```text +AUTHORIZED + ↓ +exact protected execution + ↓ +EXECUTED + ↓ +authoritative actual input derived + ↓ +maxInput enforced + ↓ +authenticated exerciser pays + ↓ +input PoolManager delta closed + ↓ +PoolManager delivers exactly q +to authoritative Beneficiary + ↓ +output PoolManager delta closed + ↓ +context remains EXECUTED +``` + +Production then requires F8D before successful completion: + +```text +F8D absent + ↓ +context still EXECUTED + ↓ +production completion barrier fails + ↓ +entire exercise transaction reverts +``` + +This is the intended F8C endpoint. + +--- + +## 26. Project Status Consequence + +After independent G8C closure, the authorized implementation frontier advances to: + +> **F8D — O2 Causal Finalization / Remaining Entitlement Reduction** + +Claude was subsequently instructed to update `docs/project-status.md` **status only**: + +- F8C COMPLETE; +- G8C PASS; +- F8D next authorized implementation slice / current blocker. + +Claude was prohibited from adding implementation detail, gate reasoning, retrospective observations, or other descriptive material to the project-status artifact. + +The corresponding follow-up instruction and action were also recorded in the Session 13 Claude log for audit chronology. + +--- + +## 27. Methodology Observations + +### 27.1 Exact-delta review improves independent verification + +Session 13 provides evidence for a potentially useful methodology refinement: + +> **Independent implementation review is strongest when performed against the exact version-controlled delta between the last gated state and the proposed next gated state, rather than against implementation-agent summaries or independently supplied final files.** + +The Git diff made both additions and unintended modifications visible within the same review surface. + +A possible compact formulation for later retrospective consideration is: + +> **Gate Review Provenance = Known Gated Base + Exact Candidate Delta + Independent Verification** + +This is a retrospective candidate only and is **not** frozen methodology. + +### 27.2 Structural unreachability can be stronger than artificial negative-path construction + +G8C-10 exposed a useful verification distinction. + +Some invalid states cannot be produced through the production interface precisely because the implementation structurally prevents the necessary degree of freedom. + +In such cases, adding test-only mutation points solely to manufacture the invalid state can weaken fidelity between the tested system and production. + +The stronger evidence may instead be: + +```text +single authoritative source ++ no alternate input path ++ exact propagation ++ authoritative downstream postcondition +``` + +A possible retrospective formulation is: + +> **When an invalid state is structurally unreachable through the production interface, verification may be better discharged by proving the absence of an alternate construction path and enforcing an authoritative downstream postcondition than by introducing test-only semantics solely to manufacture the invalid state.** + +This is also a retrospective candidate, not a frozen methodology rule. + +### 27.3 Slice-completion safety is distinct from slice responsibility + +F8C revealed that a slice can correctly implement its local responsibility while still being unsafe to allow as a durable top-level transaction before the next responsibility exists. + +The production completion barrier solved that problem without transferring F8D responsibility into F8C. + +This suggests a useful distinction for later methodology analysis: + +```text +Local Responsibility Completeness + ≠ +Safe Intermediate Deployability +``` + +A slice boundary may therefore require a fail-closed completion condition when the next slice owns an economically atomic consequence. + +This observation strongly reinforces the existing Economic Atomicity and Verification-Gated Dependencies principles. + +### 27.4 Test harnesses require semantic-boundary review + +The contrast between: + +- `ExerciseDeltaClosureRouter`; and +- `UnfinalizedExerciseRouter` + +demonstrated that a test harness can be valid evidence for one slice and invalid evidence for a later slice if its simplifications cross the later slice's responsibility boundary. + +Therefore test infrastructure itself should be evaluated against the responsibility being proven, not assumed reusable merely because it exercises the same production component. + +--- + +## 28. Session Outcome + +Session 13 completed the full implementation-slice workflow: + +```text +reconstruct F8C responsibility + ↓ +derive authoritative settlement semantics + ↓ +derive payer and Beneficiary boundaries + ↓ +derive G8C + ↓ +discover production completion hazard + ↓ +derive F8C completion barrier + ↓ +prepare bounded Claude implementation prompt + ↓ +Claude implementation + ↓ +Claude verification + ↓ +independent exact-diff review + ↓ +G8C PASS / CLOSED + ↓ +status-only project update + ↓ +retrospective record +``` + +The implementation frontier is now: + +> **F8D — O2 Causal Finalization / Remaining Entitlement Reduction** + +F8D should begin from the newly gated F8C state and must preserve the established responsibility decomposition rather than reinterpret F8A, F8B, or F8C. + +--- + +## 29. Evidence Chain + +The Session 13 evidence chain consists of: + +1. frozen Standby canonical artifacts; +2. frozen Uniswap v4 realization and implementation plan; +3. previously gated F8A and F8B implementation state; +4. Session 13 F8C Claude implementation prompt; +5. `docs/prompts/session-13-log.md`; +6. exact Git implementation delta used for independent review; +7. this ChatGPT reasoning record; +8. G8C independent PASS / CLOSED determination; +9. F8C implementation commit and pull request. + +The temporary uploaded diff used during independent review is not itself required as a repository artifact because the Git commit and pull request preserve the authoritative code delta. + +--- + +**End of Session 13 ChatGPT Reasoning Record** diff --git a/docs/prompts/session-13-f8c-authoritative-settlement-delivery.md b/docs/prompts/session-13-f8c-authoritative-settlement-delivery.md new file mode 100644 index 0000000..1ca7c81 --- /dev/null +++ b/docs/prompts/session-13-f8c-authoritative-settlement-delivery.md @@ -0,0 +1,1093 @@ +# Standby — Session 13 + +## F8C — Authoritative Settlement / Direct Beneficiary Delivery + +We are continuing implementation of **Standby**, the ETHGlobal 2026 Uniswap v4 reference realization. + +This Claude session corresponds only to: + +> **F8C — Authoritative Settlement / Direct Beneficiary Delivery** + +F8A and F8B are complete and independently reviewed. + +The current authoritative implementation state is `main`. + +Do not begin F8D, GI, F9, or any later slice. + +--- + +# 1. Objective + +Implement the exact frozen F8C responsibility: + +> After the one F8B-proven protected exact-output PoolManager execution reaches Hook-owned `EXECUTED`, derive the authoritative actual input debt from the exact PoolManager execution result, enforce the originating exerciser's `maxInput` against that actual debt, settle exactly that debt directly from the authenticated exerciser to PoolManager, and discharge the corresponding protected-output PoolManager credit by transferring exactly `q` directly from PoolManager to the authoritative Beneficiary. + +F8C must leave: + +```text +Hook causal context = EXECUTED +Remaining Entitlement = unchanged +Original Entitlement = unchanged +no fulfillment consequence +no commitment-completion consequence +no F8D finalization +``` + +F8C must also preserve the slice-boundary atomicity restriction: + +> **Until F8D exists, a production F8C-only top-level `exercise()` must not successfully commit settlement and Beneficiary delivery while the Hook remains `EXECUTED`.** + +The production path must therefore remain **fail-closed at top-level completion pending F8D**, while narrowly scoped test support may be used to prove the R4 settlement/delivery mechanics independently. + +Do not implement fake finalization merely to make the F8C transaction commit. + +--- + +# 2. Repository Instruction Ownership + +Follow the permanent repository instructions defined in: + +```text +CLAUDE.md +.claude/rules/* +``` + +Do not duplicate or reinterpret those instructions here. + +This session prompt owns only the F8C slice-specific: + +```text +objective +scope +requirements +prohibitions +file boundaries +gate evidence +completion boundary +``` + +Maintain: + +```text +docs/prompts/session-13-log.md +``` + +as the F8C implementation-session log. + +Do not update: + +```text +docs/project-status.md +``` + +--- + +# 3. Required Files / Artifacts to Inspect First + +Before changing code, inspect at minimum: + +```text +CLAUDE.md +.claude/rules/* +docs/project-status.md +docs/implementation-plan.md +docs/uniswap-v4-realization.md + +src/StandbyHook.sol +src/ExerciseRouter.sol + +test/harness/ExerciseDeltaClosureRouter.sol +test/harness/AdversarialExerciseRouter.sol +test/harness/StandbyHookHarness.sol + +test/shared/BaseExerciseAuthorizationTest.t.sol +test/shared/BaseAdversarialExerciseTest.t.sol + +test/integration/ProtectedExecution.t.sol +test/integration/ExerciseAuthorization.t.sol + +test/fuzz/ProtectedExecutionFuzz.t.sol +test/fuzz/ExerciseAuthorizationFuzz.t.sol +test/fuzz/ExerciseBackingFuzz.t.sol + +test/periphery/ProtectedExecutionPerimeter.t.sol +test/periphery/ExerciseAuthorizationPerimeter.t.sol +``` + +Also inspect the **pinned installed Uniswap v4 source** directly for the exact settlement APIs and accounting behavior used by this repository. + +At minimum confirm from the installed dependency: + +```text +BalanceDelta representation +PoolManager.swap accounting semantics +PoolManager currency-delta accounting +sync(...) +settle(...) +take(...) +safe signed conversion helpers +ERC20 transfer expectations used by the selected v4 version +``` + +Do not rely on remembered historical v4 APIs. + +--- + +# 4. Frozen F8C Responsibility Boundary + +The established ExerciseRouter decomposition is: + +```text +R1 + R2 -> F8A +R3 -> F8B +R4 -> F8C +R5 -> F8D +``` + +F8C owns **R4 only**. + +R4 consists of: + +```text +authoritative actual-input derivation ++ +maxInput enforcement ++ +authenticated-exerciser-funded settlement ++ +direct PoolManager Beneficiary delivery ++ +input/output PoolManager delta closure +``` + +F8C does not authorize the exercise. + +F8C does not determine whether the swap qualifies as O2. + +F8C does not prove actual protected output equals `q`. + +F8C does not reduce Remaining. + +F8C does not finalize fulfillment. + +--- + +# 5. Authoritative Facts F8C Must Consume + +Use existing authoritative facts rather than recreating them. + +The Hook-owned transaction-scoped context already binds the authenticated O2 cause, including: + +```text +service / PoolId +commitmentId +configured ExerciseRouter +authenticated originating exerciser +authoritative Beneficiary +q +current causal state +``` + +F8B establishes: + +```text +state == EXECUTED +``` + +only after authoritative PoolManager execution evidence proves that the exact protected swap produced actual output `q`. + +F8C must consume that result. + +Do not repeat F8A authorization logic. + +Do not repeat F8B exact-execution classification logic. + +--- + +# 6. Canonical Execution Object Reuse + +F8B already obtains the Hook-authoritative protected operation through the existing execution read surface and executes it through PoolManager. + +F8C needs the exact input/output currency identities of that same executed operation. + +Preserve Single Normative Ownership. + +Prefer carrying/reusing the already obtained execution facts from the R3 execution helper rather than independently reconstructing a second protected-operation description inside R4. + +An internal helper may, where cleanly appropriate, return the execution facts it already possesses, for example conceptually: + +```text +PoolKey +SwapParams +BalanceDelta +``` + +This is an implementation-shape choice, not a required exact signature. + +The semantic requirement is: + +> F8C must derive settlement currencies from the exact canonical operation that R3 just executed, not from caller-supplied currency identities and not from an independent duplicate economic description. + +--- + +# 7. Authoritative Actual Input + +The PoolManager result is authoritative. + +Do not use: + +```text +quoted input +estimated input +requested input +prospective authorization input +maxInput +pre-funded router amount +caller-supplied amount +``` + +as actual settlement truth. + +For the executed protected swap: + +## protected zeroForOne + +```text +input currency = currency0 +output currency = currency1 +``` + +The authoritative input-side delta is `amount0()` and must represent debt owed by the PoolManager swap caller. + +The protected-output side is `amount1()` and F8B has already proven actual output == `q`. + +## protected oneForZero + +```text +input currency = currency1 +output currency = currency0 +``` + +The authoritative input-side delta is `amount1()`. + +The protected-output side is `amount0()` and F8B has already proven actual output == `q`. + +The input-side PoolManager delta must have the authoritative debt sign. + +Do not convert by blindly applying absolute value. + +Fail closed if the expected input side does not represent a negative debt. + +Use a conversion approach that cannot overflow or reinterpret the `int128` minimum value. + +Prefer widening the signed value before negation, or an installed v4/utility conversion proven equivalent. + +Document the exact conversion decision in the Session 13 log. + +--- + +# 8. Where Actual Input Lives + +Do **not** add actual input to persistent Hook state. + +Do **not** add it to the Hook causal context merely for convenience. + +The authoritative input value is already available transaction-locally from the exact PoolManager execution result at the R3 -> R4 boundary. + +Consume it directly during the same unlock callback. + +No settlement ledger is required. + +No input snapshot is required. + +--- + +# 9. `maxInput` + +The public request surface remains: + +```solidity +exercise(uint256 commitmentId, uint256 q, uint256 maxInput) +``` + +F8C activates the previously inert `maxInput`. + +The exact predicate is: + +```text +actualInput <= maxInput +``` + +Required boundaries: + +```text +actualInput < maxInput -> pass +actualInput == maxInput -> pass +actualInput > maxInput -> revert whole exercise +``` + +`maxInput` is exercise-local cost protection only. + +It must not: + +```text +affect commitment validity +affect q authorization +affect Beneficiary eligibility +affect Supporting Capacity +affect Aggregate Capacity Obligation +enter Hook economic state +enter persistent state +become a Hook causal-context field +``` + +Pass it only as far as needed to the R4 settlement stage. + +--- + +# 10. Authoritative Economic Payer + +The economic payer is: + +> **the authenticated originating exerciser already bound in the Hook-owned causal context** + +Do not infer payer merely from: + +```text +ExerciseRouter address +PoolManager swap caller +Beneficiary +tx.origin +current test-harness funding +arbitrary supplied payer +``` + +The Router coordinates payment mechanics. + +The Router is not Standby principal and must not become a pre-funded Standby reserve. + +Use the already authenticated exerciser associated with this exact O2 causal context. + +Do not add a second payer snapshot if the existing context already supplies the authoritative actor. + +--- + +# 11. Input Settlement + +Use the pinned installed v4 settlement interfaces. + +For the supported ERC20 reference domain, the intended economic sequence is conceptually: + +```text +derive actualInput + +require actualInput <= maxInput + +PoolManager.sync(inputCurrency) + +transfer exactly actualInput + from authenticated exerciser + directly to PoolManager + +PoolManager.settle() + +prove relevant Router input-side PoolManager delta == 0 +``` + +The exact installed signatures and helper libraries must be established from the repository dependency. + +Calling `settle()` alone is insufficient proof. + +The actual negative input-side PoolManager debt must be completely closed. + +--- + +# 12. Direct Funding Requirement + +The supported economic asset path is: + +```text +authenticated exerciser + -> +PoolManager +``` + +The Router may coordinate `transferFrom` / allowance mechanics. + +Do not intentionally implement: + +```text +exerciser -> Router -> PoolManager +``` + +and do not fund settlement from an existing Router balance. + +Ordinary ERC20 allowance / `transferFrom` is sufficient for the MVP if compatible with the repository's existing token/test setup. + +Permit2 is not required unless it materially improves the existing path without expanding scope. + +Do not introduce unnecessary periphery complexity. + +--- + +# 13. Exact Settlement Amount + +The amount funded must be: + +```text +exactly actualInput +``` + +Not: + +```text +maxInput +actualInput - 1 +actualInput + 1 +quote +estimate +router balance +``` + +Verification must prove both: + +```text +exact actualInput was sourced from the exerciser +``` + +and: + +```text +the relevant PoolManager input debt is zero afterwards +``` + +Do not accept underpayment or overpayment as equivalent settlement. + +--- + +# 14. Direct Beneficiary Delivery + +After successful input settlement, resolve the protected output by the canonical path: + +```text +PoolManager.take( + outputCurrency, + authoritativeBeneficiary, + q +) +``` + +The Beneficiary must come from the existing authoritative Hook-owned O2 context. + +Do not add: + +```text +recipient +beneficiary +outputCurrency +``` + +as caller-controlled exercise parameters. + +The quantity must be the existing bound `q`. + +The currency must be the exact protected output currency of the executed canonical operation. + +--- + +# 15. No Output Custody Substitution + +The supported production path must never become: + +```text +PoolManager -> ExerciseRouter -> Beneficiary +``` + +or: + +```text +PoolManager -> StandbyHook -> Beneficiary +``` + +The PoolManager output credit is taken **directly to the authoritative Beneficiary**. + +For canonical exact-transfer mocks, verification should establish: + +```text +Beneficiary balance increase == q +ExerciseRouter protected-output balance increase == 0 +StandbyHook protected-output balance increase == 0 +relevant Router PoolManager output credit == 0 +``` + +Do not create an alternate router-owned token balance as authoritative delivery evidence. + +--- + +# 16. Causal Context + +Do not add: + +```text +SETTLED +DELIVERED +actualInput +settlement nonce +delivery nonce +payer snapshot +delivery flag +persistent execution record +settlement ledger +``` + +unless an unexpected implementation fact proves one is strictly necessary. + +The expected correct result is: + +```text +context remains EXECUTED +``` + +after successful R4 mechanics. + +F8D owns eventual consumption. + +--- + +# 17. F8D Boundary — Strictly Out of Scope + +Do not implement: + +```text +Remaining Entitlement reduction +Original Entitlement mutation +Aggregate Capacity Obligation release +fulfillment marking +fulfilled flag +commitment completion +post-execution final backing confirmation +finalization +context consumption +F8D replay protection +``` + +Do not call a newly invented pseudo-finalization function merely to make F8C complete. + +F8D will later: + +```text +require matching EXECUTED context +re-read authoritative commitment +revalidate q <= current Remaining +derive actual post-swap Supporting Capacity +derive current Aggregate Capacity Obligation +derive Ofinal = Ocurrent - q +require Sactual >= Ofinal +reduce Remaining exactly q +consume context exactly once +``` + +F8C must leave that responsibility untouched. + +--- + +# 18. Critical F8C Slice-Completion Restriction + +This requirement was derived after reviewing the actual F8A/F8B transient-context implementation and is mandatory. + +If production F8C allowed the top-level exercise to return successfully before F8D exists, the following durable state could occur: + +```text +swap committed +exerciser paid +Beneficiary received q +Remaining unchanged +Hook context still EXECUTED +``` + +At transaction end, EIP-1153 transient context disappears. + +That would destroy the causal proof required by F8D while leaving the entitlement unreduced, allowing the commitment to be exercised again. + +Therefore: + +> **A production F8C-only exercise must remain fail-closed at top-level completion until F8D has consumed/finalized the EXECUTED context.** + +Implement a minimal completion barrier that preserves this rule. + +Conceptually: + +```text +exercise + -> +authorize + -> +unlock + -> +execute + -> +settle + -> +deliver + [F8D will later finalize here] + -> +require causal context consumed +``` + +At F8C, because F8D is absent: + +```text +context remains EXECUTED +=> production top-level request must revert +``` + +At F8D, the later implementation should be able to insert finalization such that: + +```text +context consumed +=> production top-level request completes +``` + +Do not move F8D semantics upstream to satisfy the barrier. + +Do not persist a fake completion flag. + +The exact Solidity shape is an implementation choice, but the behavioral property is mandatory. + +--- + +# 19. F8C Verification Strategy + +F8C must use the **real pinned PoolManager** wherever authoritative settlement/accounting behavior is under test. + +Mocks may not replace PoolManager debt/settlement truth. + +Because production remains intentionally fail-closed pending F8D, positive committed R4 mechanics may use a narrowly scoped test harness if necessary. + +Such test support must: + +```text +exercise the real production R3/R4 mechanics +use the real PoolManager +not redefine payer semantics +not redefine maxInput semantics +not redirect output to itself +not mutate Remaining +not claim fulfillment +not become production semantics +``` + +Document why the harness exists and what it does not prove. + +--- + +# 20. Existing F8B Delta-Closure Harness + +Reassess: + +```text +test/harness/ExerciseDeltaClosureRouter.sol +``` + +Its current purpose is F8B-only mechanical delta closure. + +Its behavior — pre-funded Router input settlement and Router custody of protected output — is deliberately **not** valid F8C economic semantics. + +Do not use it as positive F8C evidence. + +It may remain only where F8B-isolated tests still genuinely need it. + +Where appropriate, narrow its use or replace F8C-facing positive paths with new R4-accurate test infrastructure. + +Do not unnecessarily rewrite valid F8B evidence. + +--- + +# 21. Adversarial Threats to Verify + +At minimum test or otherwise provide evidence against: + +```text +fake router-supplied actual input +quote substituted for actual input +requested amount substituted for actual input +wrong BalanceDelta side +wrong sign +absolute-value reinterpretation +zeroForOne / oneForZero direction confusion +unsafe signed conversion +maxInput checked against anything except actual input +maxInput equality incorrectly rejected +settlement attempted before maxInput validation +underpayment +overpayment +partial settlement +wrong payer +Router-funded settlement +arbitrary third-party payer substitution +insufficient exerciser funds +insufficient exerciser allowance +wrong recipient +caller-selected recipient +Router output custody +Hook output custody +less than q delivery +greater than q delivery +wrong output currency +stale or absent EXECUTED context +settlement before EXECUTED +repeated settlement +repeated delivery +second protected execution +cross-commitment contamination +unrelated PoolManager netting +nested/interleaved O2 +relevant reentrancy +failure after swap but before settlement +failure during settlement +failure after settlement but before delivery +delivery failure +malformed O2 falling into ordinary O3 semantics +Remaining mutation before F8D +obligation release before F8D +fulfillment claim before F8D +successful production F8C-only top-level completion +``` + +Add any implementation-specific threat revealed during the work. + +--- + +# 22. Required Failure Atomicity + +All of the following must cause the entire protected exercise to revert: + +```text +actualInput > maxInput +insufficient exerciser balance +insufficient allowance +failed exact transfer +failed PoolManager settlement +non-zero residual input debt +failed direct output take/delivery +invalid settlement/delivery causal state +F8C-only production completion before F8D +``` + +After such failure, prove as applicable: + +```text +PoolManager swap does not survive +exerciser payment does not survive +Beneficiary output does not survive +Remaining unchanged +Original unchanged +obligation consequence unchanged +no reusable EXECUTED context survives the transaction +``` + +--- + +# 23. G8C — Authoritative Settlement / Delivery Gate + +Implement and provide evidence for all of the following. + +## G8C-1 — Authoritative currency identity + +Input and output currencies derive only from the exact configured protected execution. + +Prove both protected directions. + +## G8C-2 — Authoritative actual input + +Actual input derives from the negative input-side authoritative PoolManager `BalanceDelta` produced by the exact F8B swap. + +No quote, request amount, prospective value, or `maxInput` substitutes. + +## G8C-3 — Signed interpretation + +Wrong side, wrong sign, zero/non-debt input-side interpretation, and absolute-value substitution fail closed. + +## G8C-4 — Safe conversion + +Signed conversion/negation is proven safe and semantically correct for the supported `int128` PoolManager delta domain. + +## G8C-5 — Exact `maxInput` + +Prove: + +```text +actualInput < maxInput +actualInput == maxInput +actualInput > maxInput +``` + +with equality accepted and breach atomically reverted. + +## G8C-6 — Cost-protection isolation + +`maxInput` remains request-local settlement protection only. + +No Hook economic-state or causal-context contamination. + +## G8C-7 — Authenticated exerciser pays + +The exact exerciser bound by the authoritative Hook context funds the debt. + +Router, Beneficiary, arbitrary payer, and pre-funded Router balances cannot substitute. + +## G8C-8 — Exact funding + +Exactly `actualInput` moves from authenticated exerciser directly to PoolManager. + +## G8C-9 — Exact v4 settlement + +Use the pinned installed PoolManager settlement semantics correctly and prove relevant input delta is fully cleared. + +## G8C-10 — No under/over settlement + +Less or more than the authoritative debt is not accepted as the supported payment. + +## G8C-11 — Authoritative Beneficiary + +Recipient derives only from the existing Hook-owned causal context. + +Caller cannot redirect output. + +## G8C-12 — Direct exact delivery + +PoolManager transfers exactly `q` of the correct protected output currency directly to the authoritative Beneficiary. + +## G8C-13 — No Router/Hook output custody + +Production R4 does not substitute Router or Hook token custody for Beneficiary delivery. + +Relevant PoolManager output credit is closed. + +## G8C-14 — EXECUTED prerequisite + +Settlement/delivery occur only for the same O2 that reached Hook-owned `EXECUTED`. + +Stale, absent, substituted, or unrelated context cannot authorize R4. + +## G8C-15 — Single R4 sequence + +No split settlement, split delivery, repeated settlement/delivery, unrelated PoolManager activity, cross-commitment netting, or second protected execution is supported. + +## G8C-16 — F8D non-contamination + +Throughout F8C: + +```text +Remaining unchanged +Original unchanged +Aggregate Capacity Obligation consequence unchanged +no fulfillment +no completion +no context consumption +``` + +## G8C-17 — No new O2 lifecycle state + +Context remains `EXECUTED`. + +No `SETTLED`, `DELIVERED`, or equivalent persistent/transient lifecycle state is introduced. + +## G8C-18 — Full failure atomicity + +Every payment, settlement, `maxInput`, or delivery failure unwinds: + +```text +swap +payment +delivery +causal intermediate effects +``` + +with no prohibited authoritative residue. + +## G8C-19 — Slice-boundary completion safety + +Before F8D exists, the production F8C-only top-level exercise cannot successfully commit settlement and Beneficiary delivery while Remaining remains unreduced and the transient `EXECUTED` proof would disappear. + +Positive R4 mechanics may be demonstrated through narrowly isolated test infrastructure, but such infrastructure is not production O2 semantics. + +--- + +# 24. Required Test Families + +Use the repository's existing test architecture and permanent testing conventions. + +Provide F8C gate evidence across at least: + +```text +unit +integration +fuzz +periphery/adversarial +``` + +where each family adds meaningful proof rather than duplicated examples. + +Real PoolManager integration evidence is mandatory for: + +```text +actual BalanceDelta interpretation +input debt +settlement +delta closure +direct take +failure rollback +``` + +Direction-general evidence must cover `oneForZero` as well as the canonical `zeroForOne` fixture where the mechanic is direction-sensitive. + +Fuzz tests should target meaningful F8C boundaries such as: + +```text +actualInput vs maxInput +input-debt extraction +payer funding +q delivery +``` + +within supported ranges. + +Do not weaken existing F8A/F8B tests merely to accommodate F8C. + +If a prior F8B test must change because F8C legitimately supersedes its staging mechanics, document exactly why. + +--- + +# 25. Production Scope Expectations + +Production changes should remain narrowly concentrated. + +Expected likely production files: + +```text +src/ExerciseRouter.sol +``` + +and only if genuinely necessary: + +```text +src/StandbyHook.sol +``` + +A Hook change must have a causal or completion-boundary justification. + +Do not add Hook economic state. + +Do not expand unrelated F5/F6/F7 derivation logic. + +Do not change O1 or O3 semantics. + +Do not add F8D finalization. + +Test support may change as required to replace the F8B-only mechanical closure where F8C semantics now need real evidence. + +--- + +# 26. Completion Report + +At the end of implementation, provide a concise but complete F8C report covering: + +```text +Files Inspected +Files Changed +Requirements Implemented +Implementation Decisions +Pinned v4 Mechanics Confirmed +Tests Added / Changed +Commands Run +Results +Gate Evidence by G8C Condition +Known Limitations / Reachability Caveats +Scope Check +Proposed Gate Assessment +Recommended Next Step +Prompt Audit +``` + +The proposed gate assessment is advisory only. + +--- + +# 27. Required Gate Evidence + +Follow the permanent testing and verification conventions in: + +```text +CLAUDE.md +.claude/rules/* +``` + +For G8C evidence, the completed slice must at minimum report successful results for: + +```bash +forge fmt --check +forge lint +forge build +forge test +FOUNDRY_PROFILE=ci forge test +``` + +Also report contract size where required by the permanent repository process or where F8C materially changes production bytecode. + +Do not report F8C complete while required verification is failing. + +--- + +# 28. Status / Documentation Boundary + +Do not edit: + +```text +docs/project-status.md +``` + +during implementation. + +Do not change frozen canonical semantics. + +If you discover a genuine inconsistency between the frozen artifacts and the implementation needed for F8C: + +1. do not invent or reinterpret semantics; +2. record the issue clearly in the Session 13 log; +3. implement only what remains unambiguous; +4. identify the unresolved blocker explicitly in the completion report. + +--- + +# 29. Completion Boundary + +F8C is complete only when the implementation and evidence establish: + +```text +one F8B-proven protected execution + -> +authoritative actual input debt + -> +actualInput <= maxInput + -> +authenticated exerciser funds exactly actualInput + -> +PoolManager input debt exactly settled + -> +PoolManager directly delivers exactly q +to authoritative Beneficiary + -> +both relevant PoolManager currency deltas closed + -> +Hook context still EXECUTED + -> +Remaining / obligation consequence unchanged + -> +production top-level path remains fail-closed +pending F8D + -> +session-13-log.md updated +``` + +Stop at the F8C completion boundary. diff --git a/docs/prompts/session-13-log.md b/docs/prompts/session-13-log.md new file mode 100644 index 0000000..525b84c --- /dev/null +++ b/docs/prompts/session-13-log.md @@ -0,0 +1,367 @@ +# Session 13 — Material Prompt Log + +Initiating prompt: `docs/prompts/session-13-f8c-authoritative-settlement-delivery.md` + +This file is an implementation-process audit artifact. It is not a normative +specification and does not define protocol semantics. + +--- + +## Material Follow-Up Prompts + +### Prompt 1 — Synchronize `docs/project-status.md` with the reviewed F8C result + +**Instruction.** Update **status only** in `docs/project-status.md` to reflect the independently +reviewed F8C result: F8C — Authoritative Settlement / Direct Beneficiary Delivery **COMPLETE**, G8C +**PASS**, and F8D — O2 Causal Finalization / Remaining Entitlement Reduction as the **next authorized +implementation slice / current blocker**. No implementation details, design commentary, test +summaries, gate reasoning, retrospective observations, methodology observations, or other new +descriptive content may be added to that document; only the minimum edits needed to bring existing +status fields, roadmap/ladder entries, current-blocker/next-action fields, last-closed-gate fields, +and now-stale status statements into consistency. This session log is to record the instruction and +the resulting status-only action as audit chronology, without changing or expanding its normative +content, implementation report, verification evidence, proposed gate assessment, or previously +recorded evidence. No other file may be modified. + +**Consequence.** `docs/project-status.md` was updated in place, status-only: the header slice, last +closed gate and status ladder; the §2 next-authorized-slice, last-closed-gate and gate-closure +statements; the §3 ladder rows for F8C and F8D; the §7 line that still said settlement and delivery +were unimplemented; the §10 current blocker, its downstream-authorization statement and the +carried-forward limitation wording; the §18 next action; and the §19 handoff validated-state, +current-gate, next-blocker and next-step entries. No new descriptive content was introduced, no other +file was changed, and no implementation, test, or verification state changed. + +--- + +## Implementation Chronology + +1. Read `CLAUDE.md`, `.claude/rules/solidity-style.md`, `.claude/rules/testing.md`, + `docs/project-status.md`, the Session 13 prompt, `docs/implementation-plan.md` §14.0 (R1–R5 + decomposition), §15 (F8B) and §16 (F8C), and `docs/uniswap-v4-realization.md` §13.8 (RR-O2-10), + §14 (RR-O2-11 … RR-O2-15), §15 (RR-O2-16 … RR-O2-22) and §16 (RR-O2-4, RR-O2-5). +2. Inspected the current production and test surfaces named by the prompt: `src/ExerciseRouter.sol`, + `src/StandbyHook.sol`, `test/harness/ExerciseDeltaClosureRouter.sol`, + `test/harness/AdversarialExerciseRouter.sol`, `test/harness/StandbyHookHarness.sol`, + `test/shared/BaseExerciseAuthorizationTest.t.sol`, `test/shared/BaseAdversarialExerciseTest.t.sol`, + `test/shared/BaseUnbackedExerciseAuthorizationTest.t.sol`, `test/integration/ProtectedExecution.t.sol`, + `test/fuzz/ProtectedExecutionFuzz.t.sol` and `test/periphery/ProtectedExecutionPerimeter.t.sol`. +3. Inspected the pinned Uniswap v4 dependency directly rather than relying on remembered APIs. +4. Implemented R4 in `src/ExerciseRouter.sol` and the slice-completion barrier, then built the F8C + test infrastructure and the four F8C test families. + +--- + +## Implementation Decisions + +### Decision 1 — R3 returns the execution facts R4 needs + +`_executeAuthorizedExercise` now returns `(PoolKey, SwapParams, BalanceDelta)` rather than only the +delta. Settlement needs the exact currencies of exactly the operation R3 executed, and the alternative +— reconstructing a second protected-operation description inside R4 — would be a second statement of +what a Standby exercise trades. This is the shape §6 of the session prompt suggested, and it also +removed the delta-closure harness's own pre-read of the service `PoolKey`. + +### Decision 2 — signed conversion of the authoritative input debt + +The exact conversion is: + +```solidity +int256 inputDelta = _params.zeroForOne ? int256(_delta.amount0()) : int256(_delta.amount1()); + +if (inputDelta >= 0) revert ExerciseRouter__NoAuthoritativeInputDebt(inputDelta); + +actualInput = uint256(-inputDelta); +``` + +The `int128` delta amount is **widened to `int256` before negation**. Negating `int128` in place is +unsafe at `type(int128).min`, whose negation is not representable in `int128` and wraps back to +itself; widening first makes the whole signed domain the PoolManager can express convert exactly. +`uint256(-inputDelta)` is then a value in `[1, 2^127]` and cannot overflow. No absolute value is +taken anywhere, and a zero or positive input side is refused rather than reinterpreted. This is +verified directly at `type(int128).min`, at `-1`, at `0`, at a credit-signed input side, and across +the full `int128` × `int128` × direction domain by fuzz. + +### Decision 3 — the completion barrier is a completion condition, not a stub + +`exercise` requires the Hook-owned causal context to have been **consumed** (`EMPTY`) after the +unlock returns. At F8C nothing can consume it, so every production exercise reverts with +`ExerciseRouter__ExerciseNotFinalized(EXECUTED)` and the swap, settlement and delivery unwind with it. +The condition is stated as what completion actually requires rather than as a slice marker, so the +later finalization slice satisfies it by consuming the context inside the same unlock, with nothing +here to remove or revisit. No pseudo-finalization function was invented and no completion flag is +persisted. + +### Decision 4 — `maxInput` transport + +`maxInput` is carried to the settlement stage as the `unlock` payload and nowhere else. It is not +passed to the Hook, does not enter the causal context, and is not persisted. The unlock payload is +returned verbatim by the PoolManager to a callback that only the PoolManager can enter, and it is +compared against authoritative accounting rather than trusted. + +### Decision 5 — two delta-closure requirements rather than trusting `settle`/`take` + +`settle()` credits whatever actually moved and `take()` accounts whatever was actually taken, so +neither is proof. Each side is required to be closed by `TransientStateLibrary.currencyDelta(...) == 0` +immediately after the operation that was supposed to close it. `PoolManager.unlock` would also refuse +to return with any delta open, but that is a count of open deltas rather than a statement about this +exercise's own input debt and output credit. + +### Decision 6 — F8B evidence preserved, F8C evidence built alongside + +`ExerciseDeltaClosureRouter` was **not** retired. Its purpose changed rather than disappeared: F8A and +F8B evidence is about authorization and Hook-owned execution evidence *in isolation*, and observing +that a committed real execution delivers nothing to the Beneficiary now requires a router with no R4 +behind it, because production has one. It was narrowed to that role, re-parented onto the new +`UnfinalizedExerciseRouter`, and adapted to the new R3 return shape; no F8A or F8B test was changed, +weakened, or removed. + +F8C-facing positive paths use new R4-accurate infrastructure instead: `UnfinalizedExerciseRouter` is +the production `ExerciseRouter` with the completion barrier lifted and nothing else changed, and +`BaseExerciseSettlementTest` configures it, funds the exerciser with input currency only, and +deliberately keeps the router's inherited pre-funding so that "the router could have paid and did not" +is checkable rather than absent. + +### Decision 7 — the authoritative debt is obtained from production, not estimated + +`maxInput` boundary tests need the exact debt before choosing a bound. The fixture obtains it by +requesting the exercise under a cost bound of zero and decoding the debt out of production's own +`ExerciseRouter__ExerciseCostExceedsMaxInput` refusal. The refusal unwinds the swap, so the pool is +unchanged and the subsequent real request produces the same debt. A bound derived from a quote or an +arithmetic reconstruction would be a bound on a different number than the one production compares +against, and equality against the wrong number would prove nothing. + +--- + +## Pinned Uniswap v4 Mechanics Confirmed + +Read directly from the installed dependency at the pinned revisions rather than from memory: + +- `BalanceDelta` — packed `int128` pair, `amount0()`/`amount1()` attached globally, sign convention + negative = owed by the swap caller (`v4-core/src/types/BalanceDelta.sol`); +- `PoolManager.swap` accounts the resulting delta to `msg.sender`, which is the unlocking router + (`v4-core/src/PoolManager.sol`); +- `sync(Currency)` records the currency and its current reserve balance; +- `settle()` is `onlyWhenUnlocked`, computes `paid = balanceOfSelf() - reservesBefore` and credits it + to `msg.sender`, so an ERC-20 settlement is `sync` → transfer in → `settle`; +- `take(Currency, address to, uint256 amount)` is `onlyWhenUnlocked`, debits `amount` from + `msg.sender`'s delta and transfers directly to `to`; +- `TransientStateLibrary.currencyDelta(manager, target, currency)` reads a participant's outstanding + delta through `exttload`; +- `IERC20Minimal.transferFrom(address,address,uint256) returns (bool)` + (`v4-core/src/interfaces/external/IERC20Minimal.sol`); +- the pinned fixture currencies perform exact transfers with no fee and no rebasing, satisfying + RR-O2-22. + +--- + +## Reachability Caveats Recorded + +1. **Real-stack direction generality.** The canonical Standby fixture is `zeroForOne`, and the + direction-sensitive part of R4 — which currency is spent, which is produced, and which delta side + is the debt — is a pure derivation. It is verified in both directions by unit tests and across the + full signed domain by fuzz. A real-stack `oneForZero` exercise would require a mirrored service + fixture that does not exist in the repository; F8B's execution evidence carries the same caveat. +2. **Under/over settlement is unreachable by construction.** The settled amount is not a caller-supplied + quantity: it is the derived debt, and production transfers exactly it. `ExerciseRouter__UnresolvedExerciseDelta` + is the fail-closed guard that would catch either, and it is unreachable with exact-transfer + currencies. +3. **`ExerciseRouter__InputTransferFailed` is unreachable with the fixture currencies**, which revert + rather than returning `false`. Both failing payment conditions — insufficient balance and + insufficient allowance — are exercised and unwind the whole exercise. +4. **Delivery failure after successful settlement is unreachable.** `take` of exactly the proven + credit cannot fail against an exact-transfer currency; the Hook has already proven the credit is + exactly `q`. + +--- + +## Required Task Completion Report + +### Files Inspected + +Repository: + +- `CLAUDE.md`, `.claude/rules/solidity-style.md`, `.claude/rules/testing.md`; +- `docs/project-status.md`, `docs/implementation-plan.md` (§14.0, §15, §16, §17), + `docs/uniswap-v4-realization.md` (§13–§16); +- `src/ExerciseRouter.sol`, `src/StandbyHook.sol`, `src/mocks/MockFixtureCurrency.sol`, + `src/interfaces/IActorAwarePeriphery.sol`; +- `script/helpers/StandbyFixtureConfig.sol`; +- `test/harness/ExerciseDeltaClosureRouter.sol`, `test/harness/AdversarialExerciseRouter.sol`, + `test/harness/StandbyHookHarness.sol`; +- `test/shared/BaseActorAwareStandbyTest.t.sol`, `test/shared/BaseCommitmentAdmissionTest.t.sol`, + `test/shared/BaseAuthenticBackingTest.t.sol`, `test/shared/BaseExerciseAuthorizationTest.t.sol`, + `test/shared/BaseUnbackedExerciseAuthorizationTest.t.sol`, + `test/shared/BaseAdversarialExerciseTest.t.sol`, `test/shared/BaseDerivationTest.t.sol`; +- `test/integration/ProtectedExecution.t.sol`, `test/fuzz/ProtectedExecutionFuzz.t.sol`, + `test/periphery/ProtectedExecutionPerimeter.t.sol`. + +Pinned dependency: + +- `v4-core/src/PoolManager.sol`, `v4-core/src/interfaces/IPoolManager.sol`, + `v4-core/src/libraries/TransientStateLibrary.sol`, `v4-core/src/types/BalanceDelta.sol`, + `v4-core/src/interfaces/external/IERC20Minimal.sol`; +- `lib/forge-std/src/Vm.sol`. + +### Files Changed + +Production: + +- `src/ExerciseRouter.sol` — **modified.** Implements R4: the causal-context prerequisite, the + authoritative settlement derivation, the `maxInput` comparison, exerciser-funded direct settlement, + direct Beneficiary delivery, both delta-closure requirements, and the slice-completion barrier. + `_executeAuthorizedExercise` now returns the executed `PoolKey`/`SwapParams` alongside the delta. + +Test infrastructure: + +- `test/harness/UnfinalizedExerciseRouter.sol` — **new.** The production router with the completion + barrier lifted, plus two bare pass-throughs of production internals that no production path can + present malformed input to. +- `test/harness/ExerciseDeltaClosureRouter.sol` — **modified.** Re-parented onto + `UnfinalizedExerciseRouter`, adapted to the new R3 return shape, and its purpose restated: it is now + what keeps F8B evidence isolated from the settlement and delivery production performs. +- `test/shared/BaseExerciseSettlementTest.t.sol` — **new.** The F8C fixture: production R4 router, + funded and approving exerciser, the authoritative-debt probe, and the settlement state snapshot and + assertions. +- `test/shared/BaseExerciseAuthorizationTest.t.sol` — **modified, comments only.** Two doc comments + that described the pre-F8C world were brought up to date. No behavior, assertion, or fixture value + changed. + +Tests: + +- `test/unit/ExerciseSettlementDerivation.t.sol` — **new** (11 tests). +- `test/integration/ExerciseSettlement.t.sol` — **new** (15 tests). +- `test/fuzz/ExerciseSettlementFuzz.t.sol` — **new** (3 tests). +- `test/periphery/ExerciseSettlementPerimeter.t.sol` — **new** (7 tests). + +Documentation: + +- `docs/prompts/session-13-log.md` — **new.** This file. + +`docs/project-status.md` was not modified, as the session prompt requires. + +### Requirements Implemented + +R4 of the frozen ExerciseRouter decomposition (`implementation-plan.md` §14.0), realizing +`implementation-plan.md` §16.1–§16.9 and `uniswap-v4-realization.md` RR-O2-10 through RR-O2-17 and +RR-O2-19, plus the slice-boundary completion restriction of session-prompt §18. Finalization +(RR-O2-18, RR-O2-20, §17 of the implementation plan) is untouched. + +### Tests Added or Changed + +Added, by requirement: + +| Test | Proves | +| --- | --- | +| `test_protectedDirection_spendsCurrency0AndProducesCurrency1` | G8C-1 currency identity, protected direction | +| `test_oppositeDirection_spendsCurrency1AndProducesCurrency0` | G8C-1 currency identity, `oneForZero` | +| `test_creditSignedInputSide_isRefused` | G8C-3 a credit is not a debt | +| `test_zeroInputSide_isRefused` | G8C-3 zero is not a debt | +| `test_debtOnTheWrongSide_isRefused` | G8C-3 the side is chosen by the operation, not by the values | +| `test_minimumSignedDebt_convertsExactly` | G8C-4 `type(int128).min` converts rather than wrapping | +| `test_smallestSignedDebt_isSettleable` | G8C-4 the refusal boundary is exactly at zero | +| `testFuzz_authoritativeDebt_isTheExecutedDirectionsInputSide` | G8C-1 … G8C-4 across the full signed domain and both directions | +| `test_absentContext_authorizesNoResolution` | G8C-14 no context authorizes no R4 | +| `test_authorizedButUnexecutedContext_authorizesNoResolution` | G8C-14 complete bindings without execution proof authorize no R4 | +| `test_inFlightContext_authorizesNoResolution` | G8C-14 an accepted proposal is not an execution | +| `test_undecidedContext_authorizesNoResolution` | G8C-14 an undecided authorization authorizes no R4 | +| `test_authorizedExercise_settlesExactlyAndDeliversDirectly` | G8C-8, G8C-9, G8C-12, G8C-13 on the real stack | +| `test_settlementAmount_isTheAuthoritativeDebtRatherThanTheRequest` | G8C-2 the debt is neither `q` nor `maxInput` | +| `test_costBoundEqualToTheDebt_isAccepted` | G8C-5 equality passes | +| `test_costBoundAboveTheDebt_isAccepted` | G8C-5 one unit above passes | +| `test_costBoundBelowTheDebt_unwindsTheWholeExercise` | G8C-5, G8C-18 one unit below reverts atomically | +| `test_costBound_leavesNoTraceInAuthoritativeState` | G8C-6 cost protection reaches no Hook state | +| `test_delivery_followsTheExercisedCommitmentsBeneficiary` | G8C-11 recipient is Hook-owned | +| `test_delivery_followsTheOtherCommitmentsBeneficiary` | G8C-11 recipient tracks the commitment, not a constant | +| `test_settledAndDeliveredExercise_fulfilsNothing` | G8C-16 no fulfillment consequence | +| `test_causalContext_remainsExecutedAfterSettlementAndDelivery` | G8C-17 no new lifecycle state | +| `test_secondExerciseAfterSettlement_isRejected` | G8C-15 exactly one delivery | +| `test_exerciserWithoutFunds_unwindsTheWholeExercise` | G8C-7, G8C-18 an unfunded exerciser is a failed exercise, not a router-funded one | +| `test_insufficientAllowance_unwindsTheWholeExercise` | G8C-18 partial permission is not partial settlement | +| `test_productionExercise_cannotCompleteWhileUnfinalized` | G8C-19 production is fail-closed | +| `test_productionExercise_isRefusedWhateverTheRequest` | G8C-19 the barrier is not about the request | +| `testFuzz_exercise_settlesTheExactDebtAndDeliversExactlyQ` | G8C-8, G8C-12, G8C-16 at every admissible quantity | +| `testFuzz_costBound_admitsExactlyTheAuthoritativeDebt` | G8C-5, G8C-10, G8C-18 the accepted and refused regions meet at equality | +| `test_directSettlementCallback_isRefused` / `...ForTheExerciseAuthority` | G8C-14 settlement is unreachable except through the PoolManager | +| `test_fundedRouter_neitherFundsNorReceives` | G8C-7, G8C-13 the coordinator is neither payer nor custodian | +| `test_fundedThirdParty_cannotBecomeThePayer` | G8C-7 no third-party payer substitution | +| `test_thirdPartyApproval_doesNotFundAnAuthorizedExercise` | G8C-7 the payer is the authenticated exerciser | +| `test_protectedOutput_movesOnlyFromThePoolManagerToTheBeneficiary` | G8C-12, G8C-13 delivery as a closed system | +| `test_resolvedExercise_cannotBeResolvedAgain` | G8C-15 no second settlement or delivery | + +No existing test was changed, weakened, or removed. Two doc comments in +`test/shared/BaseExerciseAuthorizationTest.t.sol` were corrected because they described a world in +which no settlement existed. + +### Commands Run + +```bash +forge fmt +forge fmt --check +forge lint +forge build +forge build --sizes +forge test +FOUNDRY_PROFILE=ci forge test +``` + +### Results + +- `forge fmt --check` — clean. +- `forge lint` — clean, no findings. +- `forge build` — successful. +- `forge test` — **502 passed, 0 failed, 0 skipped** across 54 suites (466 before this slice; 36 + added). +- `FOUNDRY_PROFILE=ci forge test` — **502 passed, 0 failed, 0 skipped**, at 10,000 fuzz runs. +- Contract sizes: `ExerciseRouter` 4,128 B runtime (20,448 B margin); `StandbyHook` unchanged at + 21,255 B. + +### Gate Evidence + +**Implemented and verified:** G8C-1, G8C-2, G8C-3, G8C-4, G8C-5, G8C-6, G8C-7, G8C-8, G8C-11, +G8C-12, G8C-13, G8C-14, G8C-15, G8C-16, G8C-17, G8C-19; G8C-18 for every reachable failure +(cost-bound breach, insufficient balance, insufficient allowance), each proven to leave no residue on +balances, PoolManager state, commitment facts, obligation, or causal context. + +**Implemented, structurally sound, not directly exercised:** G8C-9's residual-input-delta guard and +G8C-10's under/over-settlement guard. The settled amount is the derived debt rather than a +caller-supplied quantity, so neither can be reached with exact-transfer currencies; the successful +commit is itself evidence that the exact debt closed the delta, since underpayment would leave a +negative delta and overpayment a positive one, and both are refused. + +**Not attempted:** anything belonging to finalization. + +### Known Limitations / Blockers + +No blockers. The four reachability caveats above are recorded rather than worked around. No frozen +document was found to be contradictory, and no unauthorized state was introduced. + +### Scope Check + +Work stayed within F8C / R4. `StandbyHook.sol` was not modified. No Hook economic state was added, no +O1 or O3 semantics changed, no F5/F6/F7 derivation logic was touched, and no finalization behavior — +Remaining Entitlement reduction, obligation release, fulfillment marking, context consumption — was +implemented. `docs/project-status.md` was not edited. + +### Proposed Gate Assessment + +**PASS (proposed, advisory only).** + +Every G8C condition has implementation and verification evidence, with the two guards noted above +unreachable by construction rather than unverified by omission. Both required profiles pass with no +failures, formatting and linting are clean, and the production path remains fail-closed pending +finalization. + +### Recommended Next Step + +F8D — O2 Causal Finalization / Remaining Entitlement Reduction. The smallest coherent next +responsibility is Hook-owned finalization that requires the matching `EXECUTED` context, re-derives +actual post-execution Supporting Capacity and the post-fulfillment obligation, reduces Remaining +Entitlement exactly once, and consumes the context — which is also exactly what makes the production +completion barrier introduced here pass. Not to be started without explicit authorization. + +### Prompt Audit + +All material follow-up instructions were recorded in this log. **One** material follow-up prompt was +issued: the post-review instruction to synchronize `docs/project-status.md` with the closed G8C +result, recorded as Prompt 1 above. It followed the implementation and gate review recorded here and +changed no implementation, test, or verification state. diff --git a/src/ExerciseRouter.sol b/src/ExerciseRouter.sol index cba89ce..a081922 100644 --- a/src/ExerciseRouter.sol +++ b/src/ExerciseRouter.sol @@ -7,7 +7,10 @@ pragma solidity 0.8.26; import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; import {IUnlockCallback} from "v4-core/interfaces/callback/IUnlockCallback.sol"; +import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.sol"; +import {TransientStateLibrary} from "v4-core/libraries/TransientStateLibrary.sol"; import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; import {PoolKey} from "v4-core/types/PoolKey.sol"; import {SwapParams} from "v4-core/types/PoolOperation.sol"; @@ -20,19 +23,24 @@ import {IActorAwarePeriphery} from "./interfaces/IActorAwarePeriphery.sol"; /// @title ExerciseRouter /// @notice The designated O2 coordinator of one Protected Execution Service. -/// @dev At implementation slice F8B this contract owns exactly three responsibilities, the first three of -/// the five the reference realization assigns it (`implementation-plan.md` §14.0): it accepts the -/// external exercise request, it preserves the originating exerciser across the call into the Hook so -/// that the Hook can recover an authenticated economic actor, and it coordinates exactly one protected -/// PoolManager execution for the authorization the Hook produced. Input settlement, Beneficiary -/// delivery, and finalization are later slices and are deliberately absent here. +/// @dev At implementation slice F8C this contract owns four of the five responsibilities the reference +/// realization assigns it (`implementation-plan.md` §14.0): it accepts the external exercise request, +/// it preserves the originating exerciser across the call into the Hook so that the Hook can recover +/// an authenticated economic actor, it coordinates exactly one protected PoolManager execution for +/// the authorization the Hook produced, and it resolves that execution — settling the actual input +/// debt from the authenticated exerciser and having the PoolManager deliver exactly `q` directly to +/// the authoritative Beneficiary. Causal finalization is a later slice and is deliberately absent. /// -/// Because they are absent, no exercise completes at this slice. A real PoolManager swap opens -/// currency deltas, and `PoolManager.unlock` refuses to return while any remain open, so the -/// coordinated execution always unwinds the request that made it. That is the honest state of the -/// implementation rather than a defect to be worked around: the deltas belong to input settlement and -/// Beneficiary delivery, which are F8C's to own, and closing them here — to anyone, in any amount — -/// would be inventing settlement semantics this slice has no authority over. +/// Because it is absent, no production exercise completes at this slice, and that is enforced rather +/// than merely observed. A completed exercise leaves the Hook's causal context `EXECUTED`, which is +/// transaction-scoped: it disappears when the transaction ends. If this contract returned +/// successfully from an unfinalized exercise, the swap, the payment, and the delivery would all +/// survive while Remaining Entitlement stayed unreduced and the causal proof that could have reduced +/// it evaporated — leaving the commitment exercisable again for the whole quantity it had already +/// been exercised for. The top-level request therefore requires the causal context to have been +/// consumed before it may return, and until finalization exists that requirement is one no exercise +/// can satisfy. The barrier is a completion condition rather than a stub: finalization will consume +/// the context inside the same unlock, and the same condition will then pass unchanged. /// /// It owns no Standby economic truth and cannot acquire any. It does not know whether the commitment /// exists, who its Beneficiary is, who may exercise it, whether it is valid, whether it is @@ -49,6 +57,13 @@ import {IActorAwarePeriphery} from "./interfaces/IActorAwarePeriphery.sol"; /// truthful its answer would have been. The router's own address is likewise never an exercise /// authority: the answer it gives is the originating caller, never itself. /// +/// Settlement does not change that. This contract moves value without ever holding any: the input is +/// pulled from the authenticated exerciser directly to the PoolManager, and the protected output is +/// transferred by the PoolManager directly to the Beneficiary. It is not a Standby reserve, it is not +/// a payer, it is not a custodian of protected output, and a balance it happens to hold is never a +/// source of settlement. Both parties to the transfer are read from the Hook-owned causal context, +/// not from the request, so there is no calldata through which a caller could nominate either. +/// /// Attribution is execution context, not protocol history. The originator is bound in transient /// storage when a request begins and cleared when it ends, so the lifecycle is /// `EMPTY -> ACTIVE(exerciser) -> EMPTY` within one transaction and no reusable identity survives it. @@ -59,6 +74,8 @@ import {IActorAwarePeriphery} from "./interfaces/IActorAwarePeriphery.sol"; /// calldata field, forwarded address, or hook payload through which another exerciser could be /// nominated — the request surface simply does not carry one. contract ExerciseRouter is IActorAwarePeriphery, IUnlockCallback { + using TransientStateLibrary for IPoolManager; + /*////////////////////////////////////////////////////////////// STATE VARIABLES //////////////////////////////////////////////////////////////*/ @@ -94,6 +111,53 @@ contract ExerciseRouter is IActorAwarePeriphery, IUnlockCallback { /// @param caller The unauthorized caller. error ExerciseRouter__NotPoolManager(address caller); + /// @notice Thrown when settlement is attempted for an execution the Hook has not proven. + /// @dev Settlement and delivery resolve one specific protected execution, and the only authority that + /// an execution occurred is the Hook's own causal context. A context in any other position is not + /// a proven execution, so there is nothing to settle and nothing to deliver. + /// @param state The causal position the Hook-owned context was actually in. + error ExerciseRouter__ExerciseNotExecuted(StandbyHook.ExerciseAuthorizationState state); + + /// @notice Thrown when the input side of an executed protected swap is not an authoritative debt. + /// @dev The PoolManager's own delta is the settlement truth, and its sign is part of that truth: only a + /// negative amount is currency the caller owes. A zero or positive input side is not a smaller + /// debt or an absolute quantity to be settled anyway — it is a delta this router cannot interpret + /// as the cost of the execution, so the whole exercise fails rather than settling a guess. + /// @param inputDelta The input-side amount the PoolManager actually reported. + error ExerciseRouter__NoAuthoritativeInputDebt(int256 inputDelta); + + /// @notice Thrown when the authoritative input debt exceeds the exerciser's own cost bound. + /// @dev Exercise-local cost protection and nothing else. It is compared against the debt the executed + /// swap actually produced, never against a quote, an estimate, or the requested quantity, and it + /// changes no commitment term, no entitlement, and no obligation (RR-O2-10, RR-O2-12). + /// @param actualInput The authoritative input debt of the executed protected swap. + /// @param maxInput The bound the request carried. + error ExerciseRouter__ExerciseCostExceedsMaxInput(uint256 actualInput, uint256 maxInput); + + /// @notice Thrown when the exact input transfer from the authenticated exerciser does not succeed. + /// @param currency The input currency of the executed protected swap. + /// @param exerciser The authenticated originating exerciser the debt is funded from. + /// @param amount The exact authoritative debt the transfer was for. + error ExerciseRouter__InputTransferFailed(address currency, address exerciser, uint256 amount); + + /// @notice Thrown when an exercise leaves an outstanding PoolManager currency delta behind. + /// @dev Calling `settle` is not proof that a debt was paid and calling `take` is not proof that a + /// credit was discharged, because both merely account whatever actually moved. The authoritative + /// statement is the delta itself, so each side is required to be closed after the operation that + /// was supposed to close it. Underpayment, overpayment, and an inexact transfer all surface here. + /// @param currency The currency whose delta is still open. + /// @param remainingDelta The outstanding amount. + error ExerciseRouter__UnresolvedExerciseDelta(address currency, int256 remainingDelta); + + /// @notice Thrown when an exercise would complete while its causal context is still unconsumed. + /// @dev The slice-completion barrier. An exercise whose causal proof has not been consumed has not + /// been finalized, and finalization is the only thing that reduces Remaining Entitlement. Letting + /// such a request return would commit the swap, the payment, and the Beneficiary delivery while + /// the entitlement they discharged stayed intact and the transaction-scoped proof of what + /// happened disappeared with the transaction. + /// @param state The causal position the Hook-owned context was left in. + error ExerciseRouter__ExerciseNotFinalized(StandbyHook.ExerciseAuthorizationState state); + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -109,40 +173,40 @@ contract ExerciseRouter is IActorAwarePeriphery, IUnlockCallback { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @notice Requests Hook-owned authorization to exercise a commitment for a quantity. + /// @notice Requests Hook-owned authorization to exercise a commitment for a quantity, and resolves it. /// @dev The request carries the three fields the frozen realization gives it — which commitment, how /// much of it, and the exerciser's own cost bound (`implementation-plan.md` §14.3). It /// deliberately does not carry the Beneficiary, the PoolKey, the protected direction, the exercise /// authority, Remaining Entitlement, Supporting Capacity, or Aggregate Capacity Obligation: those - /// are authoritative facts the Hook resolves, not request parameters. - /// - /// The third field is `maxInput`, and it remains request-surface data and nothing else through this - /// slice. It is exercise-local cost protection, enforced against the authoritative PoolManager - /// input debt an executed exact-output swap produces (RR-O2-10, RR-O2-12). That debt now exists — - /// the protected swap executes here — but deriving it authoritatively, requiring - /// `actualInput <= maxInput`, and settling it are one responsibility, and it is F8C's. The bound is - /// therefore not enforced here. Its parameter is deliberately left unnamed: the value arrives in - /// the request, reaches no code, and cannot be read, compared, forwarded to the Hook, bound into - /// the causal context, or persisted. That is not an omission to be tidied up later by whoever finds - /// it — it is the settlement boundary made structural, and the stage that can honour the bound is - /// the stage that should name it. + /// are authoritative facts the Hook resolves, not request parameters. Nor does it carry a + /// recipient, an input currency, an output currency, or an input amount — a request that could + /// name any of those could redirect delivery or substitute settlement truth. + /// + /// The third field is `maxInput`, and this is the slice that can honour it. It is exercise-local + /// cost protection, enforced against the authoritative PoolManager input debt the executed + /// exact-output swap produced (RR-O2-10, RR-O2-12), and it travels no further than the settlement + /// stage: it reaches the Hook through no path, enters no causal context, and is persisted nowhere. /// /// Authorization comes first and execution follows it, in one call, with nothing between them that /// could decide differently. The Hook's authorization leaves a transaction-scoped AUTHORIZED causal - /// context in place; the unlock that follows performs exactly one PoolManager operation against it. - /// Neither step is optional and neither can be repeated: a second unlock is impossible while one is - /// open, and the Hook refuses a second swap against a context that is no longer AUTHORIZED. + /// context in place; the unlock that follows performs exactly one PoolManager operation against it + /// and then resolves that operation's two currency deltas. Neither step is optional and neither can + /// be repeated: a second unlock is impossible while one is open, and the Hook refuses a second swap + /// against a context that is no longer AUTHORIZED. /// - /// The protected execution is not caught. There is no `try`, no failure branch, and no alternative - /// path after the PoolManager call, so a failed execution unwinds this request completely — the - /// authorization that preceded it included. That matters more than it looks: an execution marker - /// written inside a reverted PoolManager callback rolls back to AUTHORIZED, so a router that caught - /// a failed swap would hold a restored authorization it could execute against again. This one - /// cannot, because it does not survive the failure. + /// Nothing inside the unlock is caught. There is no `try`, no failure branch, and no alternative + /// path after the PoolManager call, so a failed execution, a breached cost bound, an exerciser who + /// cannot pay, or a delivery that does not land unwinds this request completely — the authorization + /// that preceded it included. That matters more than it looks: an execution marker written inside a + /// reverted PoolManager callback rolls back to AUTHORIZED, so a router that caught a failure would + /// hold a restored authorization it could execute against again. This one cannot, because it does + /// not survive the failure. /// - /// What a completed execution establishes is Hook-owned and narrow: the authorized swap actually - /// produced exactly `q`. No input has been settled, no output has been delivered to the - /// Beneficiary, no commitment has been fulfilled, and no Remaining Entitlement has been reduced. + /// What a resolved exercise establishes is Hook-owned and still narrow: the authorized swap + /// produced exactly `q`, the exerciser paid exactly what the pool charged for it, and the + /// Beneficiary holds exactly `q`. No commitment has been fulfilled and no Remaining Entitlement has + /// been reduced — which is why the request may not return here. The completion barrier requires the + /// Hook's causal context to have been consumed, and nothing at this slice can consume it. /// /// The originator context is cleared on the way out, so this router exposes no reusable identity /// afterwards. The Hook's own causal context is not cleared here, and must not be: whether it may @@ -150,34 +214,45 @@ contract ExerciseRouter is IActorAwarePeriphery, IUnlockCallback { /// that could clear it would be able to manufacture a second authorization. /// @param _commitmentId The commitment the caller is asking to exercise. /// @param _q The protected-output quantity the caller is asking to exercise. - function exercise(uint256 _commitmentId, uint256 _q, uint256 /* maxInput */ ) external { + /// @param _maxInput The most input the caller is willing to pay for that quantity. + function exercise(uint256 _commitmentId, uint256 _q, uint256 _maxInput) external { _beginExerciserContext(); i_hook.authorizeExercise(_commitmentId, _q); - i_poolManager.unlock(bytes("")); + i_poolManager.unlock(abi.encode(_maxInput)); + + _requireFinalizedExercise(); _endExerciserContext(); } - /// @notice Performs the one protected execution the Hook's active authorization admits. - /// @dev Only the PoolManager may invoke this, and it carries no parameters: the swap it performs is - /// asked of the Hook rather than chosen here or forwarded from the request. Direction, - /// exact-output mode, the quantity, and the qualification boundary are all Standby facts, and a - /// router that composed its own version of them would be a second, silently divergent statement of - /// what a Standby exercise executes. + /// @notice Performs and resolves the one protected execution the Hook's active authorization admits. + /// @dev Only the PoolManager may invoke this. The one thing it carries is the requesting exerciser's + /// own cost bound, which the PoolManager returns verbatim from the unlock this router opened; it + /// is request data, it is checked against authoritative accounting rather than trusted, and it is + /// the only value that crosses this boundary. The swap itself is asked of the Hook rather than + /// chosen here or forwarded from the request, and so are the two parties settlement moves value + /// between. Direction, exact-output mode, the quantity, the qualification boundary, the payer, and + /// the recipient are all Standby facts, and a router that composed its own version of them would + /// be a second, silently divergent statement of what a Standby exercise executes and who it is for. /// /// Nothing about proposing the right swap is what makes it acceptable. The Hook revalidates the /// operation on the authoritative PoolManager callback path against its own causal context, so this /// contract can propose and never authorize. /// - /// It is `virtual` for one reason, and the reason is not extensibility: F8B implements no - /// settlement, so the deltas this swap opens cannot be closed by any production path, and the - /// verification of a committed real-PoolManager execution requires a test-only subclass to close - /// them mechanically. Nothing in production overrides it. + /// It is `virtual` for one reason, and the reason is not extensibility: the F8B execution evidence + /// is verified against a committed real-PoolManager swap that has no settlement behind it, which + /// requires a test-only subclass to close the resulting deltas mechanically. Nothing in production + /// overrides it. + /// @param _data The encoded exercise-local cost bound of the request that opened this unlock. /// @return result The encoded balance delta of the performed execution. - function unlockCallback(bytes calldata) external virtual returns (bytes memory result) { - result = abi.encode(_executeAuthorizedExercise()); + function unlockCallback(bytes calldata _data) external virtual returns (bytes memory result) { + (PoolKey memory key, SwapParams memory params, BalanceDelta delta) = _executeAuthorizedExercise(); + + _resolveExecutedExercise(key, params, delta, abi.decode(_data, (uint256))); + + result = abi.encode(delta); } /// @inheritdoc IActorAwarePeriphery @@ -195,14 +270,163 @@ contract ExerciseRouter is IActorAwarePeriphery, IUnlockCallback { /// /// The PoolManager authentication is here rather than in the callback entry point so that a /// test-only subclass which must close deltas cannot reach the execution without it. - function _executeAuthorizedExercise() internal returns (BalanceDelta delta) { + /// + /// It returns the operation it performed as well as the delta that operation produced, because + /// settlement needs the exact currencies of exactly this execution. Reconstructing a second + /// description of the protected operation to answer that question would create a second statement + /// of what an exercise trades, and the two could disagree. + function _executeAuthorizedExercise() + internal + returns (PoolKey memory key, SwapParams memory params, BalanceDelta delta) + { if (msg.sender != address(i_poolManager)) revert ExerciseRouter__NotPoolManager(msg.sender); - (PoolKey memory key, SwapParams memory params) = i_hook.authorizedProtectedExecution(); + (key, params) = i_hook.authorizedProtectedExecution(); delta = i_poolManager.swap(key, params, bytes("")); } + /// @dev Settles the executed protected swap's input debt and delivers its protected output. + /// + /// The order is the economic one and every step of it is load-bearing. The Hook's causal context + /// is read first, because it is the only authority that this execution happened and the only + /// source of the two parties involved: the authenticated exerciser who pays and the authoritative + /// Beneficiary who is paid. Neither is inferred from the PoolManager, from this router's address, + /// or from anything the request supplied. + /// + /// The cost bound is checked before any value moves, so a breached bound cannot leave a partial + /// settlement behind even momentarily. Input is settled before output is delivered, because taking + /// the protected output first would hand the Beneficiary currency drawn against a debt that had + /// not yet been paid. + /// + /// Both sides are then required to be closed. The PoolManager would refuse to return from the + /// unlock with any delta outstanding, so an unresolved side could never commit — but that refusal + /// is a count of open deltas rather than a statement about this exercise, and Standby's requirement + /// is specifically that this execution's own input debt and output credit were resolved, exactly, + /// by these two operations. + function _resolveExecutedExercise( + PoolKey memory _key, + SwapParams memory _params, + BalanceDelta _delta, + uint256 _maxInput + ) internal { + StandbyHook.ExerciseAuthorizationContext memory context = i_hook.exerciseAuthorization(); + + if (context.state != StandbyHook.ExerciseAuthorizationState.EXECUTED) { + revert ExerciseRouter__ExerciseNotExecuted(context.state); + } + + (Currency inputCurrency, Currency outputCurrency, uint256 actualInput) = + _executedExerciseSettlement(_key, _params, _delta); + + if (actualInput > _maxInput) revert ExerciseRouter__ExerciseCostExceedsMaxInput(actualInput, _maxInput); + + _settleExerciseInput(inputCurrency, context.exerciser, actualInput); + + _deliverProtectedOutput(outputCurrency, context.beneficiary, context.q); + } + + /// @dev Derives the settlement currencies and the authoritative input debt of an executed swap. + /// + /// Everything here comes from the operation the PoolManager actually performed and the delta it + /// actually produced. Which currency is spent and which is received follows from the direction of + /// that operation and from nothing else — a protected `zeroForOne` execution spends `currency0` + /// and produces `currency1`, and `oneForZero` is its mirror — so no caller-supplied currency + /// identity is consulted or could be. + /// + /// The debt is the input side of the authoritative `BalanceDelta`, and its sign is part of what + /// makes it authoritative: a negative amount is currency the swap caller owes the PoolManager, + /// which is the only thing this router may settle. A zero or positive input side is refused rather + /// than reinterpreted, because taking an absolute value would turn a credit into a payment and a + /// wrong-sided read into a plausible-looking amount. + /// + /// The negation widens to `int256` first. A v4 delta amount is `int128`, and negating `int128` at + /// its minimum value overflows in place; widening before negating cannot, so the whole signed + /// domain the PoolManager can express is converted exactly or refused, never wrapped. + function _executedExerciseSettlement(PoolKey memory _key, SwapParams memory _params, BalanceDelta _delta) + internal + pure + returns (Currency inputCurrency, Currency outputCurrency, uint256 actualInput) + { + int256 inputDelta; + + if (_params.zeroForOne) { + (inputCurrency, outputCurrency, inputDelta) = (_key.currency0, _key.currency1, int256(_delta.amount0())); + } else { + (inputCurrency, outputCurrency, inputDelta) = (_key.currency1, _key.currency0, int256(_delta.amount1())); + } + + if (inputDelta >= 0) revert ExerciseRouter__NoAuthoritativeInputDebt(inputDelta); + + actualInput = uint256(-inputDelta); + } + + /// @dev Funds exactly the authoritative input debt from the authenticated exerciser to the PoolManager. + /// + /// The path is deliberately direct. `sync` records what the PoolManager holds, the exerciser's own + /// currency is moved straight into it, and `settle` credits the difference — so the currency never + /// passes through this router, and a balance this router happens to hold can never become the + /// source of a Standby settlement. The allowance mechanics are the only part of this the router + /// coordinates, and an allowance is not custody. + /// + /// The amount is exactly the debt. Not the bound the request carried, not what the pool was + /// expected to charge, and not what this router could afford. + function _settleExerciseInput(Currency _inputCurrency, address _exerciser, uint256 _actualInput) internal { + i_poolManager.sync(_inputCurrency); + + bool paid = IERC20Minimal(Currency.unwrap(_inputCurrency)).transferFrom( + _exerciser, address(i_poolManager), _actualInput + ); + + if (!paid) { + revert ExerciseRouter__InputTransferFailed(Currency.unwrap(_inputCurrency), _exerciser, _actualInput); + } + + i_poolManager.settle(); + + _requireResolvedDelta(_inputCurrency); + } + + /// @dev Discharges the protected-output credit by having the PoolManager pay the Beneficiary directly. + /// + /// One operation, for the exact authorized quantity, to the account the Hook resolved from + /// authoritative commitment state (RR-O2-16, RR-O2-17). The output does not pass through this + /// router on its way there and there is no supported path by which it could: no split delivery, no + /// redirection, no claim tokens, and no intermediate custody to withdraw from later. + function _deliverProtectedOutput(Currency _outputCurrency, address _beneficiary, uint256 _q) internal { + i_poolManager.take(_outputCurrency, _beneficiary, _q); + + _requireResolvedDelta(_outputCurrency); + } + + /// @dev Requires this router's PoolManager delta in one currency to be fully closed. + function _requireResolvedDelta(Currency _currency) internal view { + int256 remainingDelta = i_poolManager.currencyDelta(address(this), _currency); + + if (remainingDelta != 0) { + revert ExerciseRouter__UnresolvedExerciseDelta(Currency.unwrap(_currency), remainingDelta); + } + } + + /// @dev Requires the exercise's causal proof to have been consumed before the request may return. + /// + /// The condition is stated as what completion actually requires rather than as a slice marker, so + /// the slice that consumes the context satisfies it by doing its own work and nothing here has to + /// be revisited or removed. Until then every production exercise fails here, atomically, with the + /// swap, the settlement, and the delivery unwound with it. + /// + /// It is `virtual` for one reason, and the reason is not extensibility: verifying that the + /// settlement and delivery mechanics themselves are correct requires observing a committed + /// exercise, which no production path can produce while finalization does not exist. Nothing in + /// production overrides it. + function _requireFinalizedExercise() internal view virtual { + StandbyHook.ExerciseAuthorizationState state = i_hook.exerciseAuthorization().state; + + if (state != StandbyHook.ExerciseAuthorizationState.EMPTY) { + revert ExerciseRouter__ExerciseNotFinalized(state); + } + } + /// @dev Binds the caller as the originator of a new exercise request. /// /// A context that is already active is rejected rather than replaced or stacked, so exactly one diff --git a/test/fuzz/ExerciseSettlementFuzz.t.sol b/test/fuzz/ExerciseSettlementFuzz.t.sol new file mode 100644 index 0000000..c9b737e --- /dev/null +++ b/test/fuzz/ExerciseSettlementFuzz.t.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {toBalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {SwapParams} from "v4-core/types/PoolOperation.sol"; + +import {StandbyFixtureConfig} from "../../script/helpers/StandbyFixtureConfig.sol"; + +import {ExerciseRouter} from "../../src/ExerciseRouter.sol"; + +import {BaseExerciseSettlementTest} from "../shared/BaseExerciseSettlementTest.t.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Fuzz evidence that the authoritative settlement derivation is exact across the whole signed +/// delta domain and both protected directions (G8C-1 through G8C-4). +/// @dev The derivation is a side selection, a sign decision, and a conversion, and each of those is the +/// kind of thing that is right almost everywhere and wrong at an edge. Both delta halves are fuzzed +/// independently across the full `int128` range, in both directions, so no run can pass by reading +/// the correct side for the wrong reason or by being handed a comfortable value. +/// +/// The expected outcome of every run is computed from the frozen meaning of a v4 delta — the input +/// side is the one the executed direction spends, and a negative amount is what the swap caller owes — +/// never from the production branch structure. +contract ExerciseSettlementDerivationFuzzTest is BaseExerciseSettlementTest { + /*////////////////////////////////////////////////////////////// + AUTHORITATIVE DEBT DERIVATION + //////////////////////////////////////////////////////////////*/ + + /// @notice The debt is the executed direction's input side, and only when that side is a debt. + function testFuzz_authoritativeDebt_isTheExecutedDirectionsInputSide( + bool _zeroForOne, + int128 _amount0, + int128 _amount1 + ) public { + SwapParams memory params = _exactOutputSwapParams(_zeroForOne, 1); + + int256 expectedInputSide = _zeroForOne ? int256(_amount0) : int256(_amount1); + + if (expectedInputSide >= 0) { + vm.expectRevert( + abi.encodeWithSelector( + ExerciseRouter.ExerciseRouter__NoAuthoritativeInputDebt.selector, expectedInputSide + ) + ); + settlementExerciseRouter.executedExerciseSettlement( + servicePoolKey, params, toBalanceDelta(_amount0, _amount1) + ); + + return; + } + + (Currency inputCurrency, Currency outputCurrency, uint256 actualInput) = settlementExerciseRouter + .executedExerciseSettlement(servicePoolKey, params, toBalanceDelta(_amount0, _amount1)); + + assertEq( + Currency.unwrap(inputCurrency), + _zeroForOne ? address(ustb) : address(usdc), + "the spent currency must be the one the executed direction spends" + ); + assertEq( + Currency.unwrap(outputCurrency), + _zeroForOne ? address(usdc) : address(ustb), + "the produced currency must be the one the executed direction produces" + ); + assertEq(actualInput, uint256(-expectedInputSide), "the debt must be the exact magnitude of the input side"); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Builds an exact-output swap in either direction, confined to that direction's boundary. + function _exactOutputSwapParams(bool _zeroForOne, uint256 _amountOut) + internal + pure + returns (SwapParams memory params) + { + params = _swapParams( + _zeroForOne, int256(_amountOut), _zeroForOne ? StandbyFixtureConfig.TICK_Q : StandbyFixtureConfig.TICK_O + ); + } +} + +/// @notice Fuzz evidence that a real exercise settles and delivers exactly, under every cost bound +/// (G8C-2, G8C-5, G8C-8, G8C-10, G8C-12, G8C-16). +/// @dev The counterpart to the derivation properties above, on the real stack and with nothing seeded: an +/// authentic commitment, the production authorization, the production execution, and the production +/// settlement and delivery against the real pinned PoolManager. +/// +/// Both fuzzed dimensions are the ones a wrong implementation would get away with at a chosen point. +/// The quantity is explored across the whole admissible extent, and the cost bound across a range that +/// straddles the authoritative debt from below and above — where the debt itself is obtained from +/// production's own refusal rather than from an estimate, so the boundary is the real one. +contract ExerciseSettlementQuantityFuzzTest is BaseExerciseSettlementTest { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev The canonical commitment entitlement: 50,000 MockUSDC. + uint128 internal constant CANONICAL_ENTITLEMENT = uint128(StandbyFixtureConfig.CANONICAL_COMMITMENT_Q); + + /// @dev The exercise quantity used where the quantity is not what is being fuzzed: 20,000 MockUSDC. + uint256 internal constant EXERCISE_Q = 20_000_000_000; + + /*////////////////////////////////////////////////////////////// + EXACT SETTLEMENT AND DELIVERY + //////////////////////////////////////////////////////////////*/ + + /// @notice Every admissible quantity settles the exact debt and delivers exactly that quantity. + function testFuzz_exercise_settlesTheExactDebtAndDeliversExactlyQ(uint256 _qSeed) public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 q = _bound(_qSeed, 1, CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, q); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, q); + + _assertExactSettlementAndDelivery(before, commitmentExerciseAuthority, commitmentId, q, actualInput); + _assertNoFulfillmentConsequence( + before, commitmentExerciseAuthority, commitmentId, "no quantity may fulfil anything" + ); + } + + /*////////////////////////////////////////////////////////////// + EXACT COST BOUND + //////////////////////////////////////////////////////////////*/ + + /// @notice An exercise proceeds if and only if the authoritative debt is within the requested bound. + /// @dev The bound is fuzzed on both sides of the real debt, so the accepted and refused regions meet + /// exactly at equality. A run that is refused must be refused for the debt production derived — + /// the reported pair is checked, not just the selector — and must leave nothing behind. + function testFuzz_costBound_admitsExactlyTheAuthoritativeDebt(uint256 _boundSeed) public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + uint256 maxInput = _bound(_boundSeed, actualInput - 1000, actualInput + 1000); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + if (actualInput > maxInput) { + vm.expectRevert( + abi.encodeWithSelector( + ExerciseRouter.ExerciseRouter__ExerciseCostExceedsMaxInput.selector, actualInput, maxInput + ) + ); + } + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q, maxInput); + + if (actualInput > maxInput) { + _assertNoExerciseResidue(before, commitmentExerciseAuthority, commitmentId); + } else { + _assertExactSettlementAndDelivery( + before, commitmentExerciseAuthority, commitmentId, EXERCISE_Q, actualInput + ); + } + } +} diff --git a/test/harness/ExerciseDeltaClosureRouter.sol b/test/harness/ExerciseDeltaClosureRouter.sol index c65e47a..19ee838 100644 --- a/test/harness/ExerciseDeltaClosureRouter.sol +++ b/test/harness/ExerciseDeltaClosureRouter.sol @@ -10,41 +10,42 @@ import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; import {Currency} from "v4-core/types/Currency.sol"; import {PoolKey} from "v4-core/types/PoolKey.sol"; -import {ExerciseRouter} from "../../src/ExerciseRouter.sol"; import {StandbyHook} from "../../src/StandbyHook.sol"; +import {UnfinalizedExerciseRouter} from "./UnfinalizedExerciseRouter.sol"; + /*////////////////////////////////////////////////////////////// CONTRACTS //////////////////////////////////////////////////////////////*/ /// @notice Closes the PoolManager currency deltas a real protected execution opens, mechanically. /// @dev A real Uniswap v4 swap opens currency deltas, and `PoolManager.unlock` refuses to return while any -/// remain open. Closing them is input settlement and Beneficiary delivery, which belong to F8C, so no -/// production path at F8B can close them and no production O2 transaction can commit. Verifying that -/// an otherwise valid real-PoolManager execution actually produced exactly `q`, and that the pool it -/// left behind is the pool the F5 derivation predicted, requires the execution to commit — which is -/// what this contract exists for, and the only thing it exists for. +/// remain open. Production now closes them — settlement and direct Beneficiary delivery are +/// implemented — and that is exactly why this contract still exists: the F8B evidence is about what +/// Hook-owned execution evidence means *on its own*, and observing that requires a committed real +/// execution with no settlement and no delivery behind it. Production can no longer produce one. /// -/// It is the production `ExerciseRouter` with one override. `exercise`, the originator attribution, the -/// authorization request, the unlock, the PoolManager authentication, and the protected execution -/// itself are all inherited production code running unmodified; the override adds nothing before the -/// execution and only closes whatever deltas that execution left afterwards. +/// It is the unfinalized production router with one further override. `exercise`, the originator +/// attribution, the authorization request, the unlock, the PoolManager authentication, and the +/// protected execution itself are all inherited production code running unmodified; the override adds +/// nothing before the execution and only closes whatever deltas that execution left afterwards, +/// instead of the production settlement and delivery it replaces. /// /// The closure is deliberately economics-free, and every choice in it is made so that nothing here can /// be read as protocol behavior: /// /// - it funds the input debt from this contract's own pre-funded balance, so it assigns no payer and -/// establishes nothing about who is supposed to pay (RR-O2-11 is F8C's); -/// - it compares nothing against `maxInput`, so it enforces no cost bound (RR-O2-12 is F8C's); +/// establishes nothing about who is supposed to pay (RR-O2-11 is the production path's); +/// - it compares nothing against `maxInput`, so it enforces no cost bound (RR-O2-12 likewise); /// - it takes the protected output to itself and not to the Beneficiary, so no test can mistake a -/// closed delta for delivery (RR-O2-16, RR-O2-17 are F8C's); +/// closed delta for delivery (RR-O2-16, RR-O2-17 likewise); /// - it reduces no Remaining Entitlement, attributes no fulfillment, and finalizes nothing. /// /// Harness-only behavior is not evidence about production settlement, delivery, payment /// responsibility, or fulfillment, and nothing here may be presented as such. What it is evidence for /// is narrow and real: the Hook's O2 classification and execution evidence, observed across a /// committed swap performed by the real pinned PoolManager. -contract ExerciseDeltaClosureRouter is ExerciseRouter { +contract ExerciseDeltaClosureRouter is UnfinalizedExerciseRouter { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -60,20 +61,18 @@ contract ExerciseDeltaClosureRouter is ExerciseRouter { /// @notice Deploys the closure router against the same Hook the production router binds. /// @param _hook The StandbyHook that owns the Protected Execution Service. - constructor(StandbyHook _hook) ExerciseRouter(_hook) {} + constructor(StandbyHook _hook) UnfinalizedExerciseRouter(_hook) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ /// @notice Runs the production protected execution and then closes its deltas mechanically. - /// @dev The pool is read before the execution because the Hook describes the admitted execution only - /// while the causal context is still AUTHORIZED — afterwards it is evidence, not a proposal. + /// @dev The pool the execution was performed against comes back from the production execution helper + /// itself, so this contract composes no description of the protected operation of its own. /// @return result The encoded balance delta of the performed execution. function unlockCallback(bytes calldata) external override returns (bytes memory result) { - PoolKey memory key = i_hook.protectedExecutionService().poolKey; - - BalanceDelta delta = _executeAuthorizedExercise(); + (PoolKey memory key,, BalanceDelta delta) = _executeAuthorizedExercise(); _closeDelta(key.currency0, delta.amount0()); _closeDelta(key.currency1, delta.amount1()); diff --git a/test/harness/UnfinalizedExerciseRouter.sol b/test/harness/UnfinalizedExerciseRouter.sol new file mode 100644 index 0000000..1688106 --- /dev/null +++ b/test/harness/UnfinalizedExerciseRouter.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {SwapParams} from "v4-core/types/PoolOperation.sol"; + +import {ExerciseRouter} from "../../src/ExerciseRouter.sol"; +import {StandbyHook} from "../../src/StandbyHook.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Lets a production settlement and delivery sequence commit while finalization does not exist. +/// @dev The production `ExerciseRouter` refuses to return from an exercise whose causal proof has not been +/// consumed, and nothing at this slice can consume it, so no production exercise commits. That refusal +/// is a Standby safety requirement rather than an obstacle — but it also means the settlement and +/// delivery mechanics it protects could not otherwise be observed at all, because every transaction +/// that performs them is unwound before anything can be measured. +/// +/// This contract is that observation and nothing more. It is the production `ExerciseRouter` with the +/// completion barrier lifted and with no other difference whatsoever: `exercise`, the originator +/// attribution, the authorization request, the unlock, the PoolManager authentication, the protected +/// execution, the causal-context read, the input-debt derivation, the `maxInput` comparison, the +/// exerciser-funded settlement, the direct Beneficiary delivery, and both delta-closure requirements +/// are all inherited production code running unmodified. +/// +/// What it removes is exactly one thing, and removing it establishes exactly one thing: that an +/// exercise which has settled and delivered has not been finalized. It does not reduce Remaining +/// Entitlement, release any obligation, attribute any fulfillment, or consume the Hook's causal +/// context — because it implements none of that, and neither does the production code it inherits. A +/// committed exercise here leaves the causal context `EXECUTED`, which is precisely the state the +/// production barrier exists to refuse. +/// +/// Nothing observed through this contract is evidence that a production O2 completes, and no test may +/// present it as such. What it is evidence for is what the production R3 and R4 mechanics do, measured +/// against the real pinned PoolManager, at the moment they have done it. +/// +/// It also exposes the production input-debt derivation, which is `internal pure` in production and +/// reachable only from inside a PoolManager unlock behind a real swap. That is a bare pass-through: it +/// adds no check, removes none, and re-implements nothing. It exists because the derivation's whole +/// job is to interpret a signed `BalanceDelta` correctly, and the real PoolManager will only ever hand +/// it well-formed ones — so the malformed cases the derivation must refuse are unreachable without it. +contract UnfinalizedExerciseRouter is ExerciseRouter { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /// @notice Deploys the unfinalized router against the same Hook the production router binds. + /// @param _hook The StandbyHook that owns the Protected Execution Service. + constructor(StandbyHook _hook) ExerciseRouter(_hook) {} + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Runs the production derivation of an executed exercise's settlement facts. + /// @param _key The pool the execution was performed against. + /// @param _params The executed protected swap. + /// @param _delta The balance delta presented as the execution's authoritative result. + /// @return inputCurrency The currency the execution spends. + /// @return outputCurrency The currency the execution produces. + /// @return actualInput The authoritative input debt of the execution. + function executedExerciseSettlement(PoolKey calldata _key, SwapParams calldata _params, BalanceDelta _delta) + external + pure + returns (Currency inputCurrency, Currency outputCurrency, uint256 actualInput) + { + (inputCurrency, outputCurrency, actualInput) = _executedExerciseSettlement(_key, _params, _delta); + } + + /// @notice Runs the production resolution of an executed exercise. + /// @dev The other bare pass-through, and it exists for the causal prerequisite rather than for the + /// mechanics. Production reaches this only from inside its own unlock, immediately after a swap the + /// Hook has already accepted and proven — so the causal positions it must refuse are exactly the + /// ones that cannot occur there, and refusing them is verifiable only by presenting them directly. + /// + /// A refused position is refused before any PoolManager interaction, which is what makes the + /// refusals observable outside an unlock at all. + /// @param _key The pool the execution claims to have been performed against. + /// @param _params The swap the resolution claims to concern. + /// @param _delta The balance delta presented as the execution's authoritative result. + /// @param _maxInput The exercise-local cost bound presented for the resolution. + function resolveExecutedExercise( + PoolKey calldata _key, + SwapParams calldata _params, + BalanceDelta _delta, + uint256 _maxInput + ) external { + _resolveExecutedExercise(_key, _params, _delta, _maxInput); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Lifts the completion barrier, so a settled and delivered exercise commits unfinalized. + function _requireFinalizedExercise() internal view override {} +} diff --git a/test/integration/ExerciseSettlement.t.sol b/test/integration/ExerciseSettlement.t.sol new file mode 100644 index 0000000..3ee6bdc --- /dev/null +++ b/test/integration/ExerciseSettlement.t.sol @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {StandbyFixtureConfig} from "../../script/helpers/StandbyFixtureConfig.sol"; + +import {ExerciseRouter} from "../../src/ExerciseRouter.sol"; +import {StandbyHook} from "../../src/StandbyHook.sol"; +import {MockFixtureCurrency} from "../../src/mocks/MockFixtureCurrency.sol"; + +import {BaseExerciseSettlementTest} from "../shared/BaseExerciseSettlementTest.t.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Integration evidence for F8C authoritative settlement and direct Beneficiary delivery on the +/// real execution stack (G8C-1, G8C-2, G8C-5 through G8C-13, G8C-16 through G8C-18). +/// @dev Every exercise here is authorized by the production Hook, executed by the real pinned +/// `PoolManager` against a real pool holding real liquidity, for a commitment the production O1 +/// transition admitted, and settled and delivered by the production R4 code. Nothing about the debt, +/// the payment, or the delivery is seeded, quoted, or estimated: the amount that moves is the amount +/// the pool actually charged, and the account it moves to is the one the Hook resolved from the +/// commitment record. +/// +/// One thing is not production: the configured router's completion barrier is lifted, because a +/// production exercise deliberately refuses to return while its causal proof is unconsumed and would +/// therefore unwind the very settlement these tests measure. That barrier is verified against the +/// production router in `ProductionExerciseCompletionTest` below, and nothing here may be read as +/// evidence that a production O2 completes. +contract ExerciseSettlementTest is BaseExerciseSettlementTest { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev The canonical commitment entitlement: 50,000 MockUSDC. + uint128 internal constant CANONICAL_ENTITLEMENT = uint128(StandbyFixtureConfig.CANONICAL_COMMITMENT_Q); + + /// @dev The exercise quantity used wherever the test is not about the quantity: 20,000 MockUSDC. + uint256 internal constant EXERCISE_Q = 20_000_000_000; + + /*////////////////////////////////////////////////////////////// + G8C-1, G8C-2, G8C-7 THROUGH G8C-13 — EXACT RESOLUTION + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves an exercise pays the exact debt from the exerciser and delivers exactly `q` to the + /// Beneficiary, with no intermediate custody anywhere. + /// @dev The canonical positive case, end to end on the real stack. The debt is the one production + /// itself reports for this exact swap, so the equality asserted is against authoritative + /// accounting rather than against a reconstruction; every currency movement is then required to + /// match on both sides at once — what left the exerciser is what reached the PoolManager, and what + /// left the PoolManager is what reached the Beneficiary. + /// + /// The router holds a large balance of both currencies throughout and the Hook holds none, which + /// is what makes "the router could have paid and did not" and "nothing passed through either of + /// them" observable rather than assumed. + function test_authorizedExercise_settlesExactlyAndDeliversDirectly() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + assertGt(before.routerInput, actualInput, "the router must be able to pay, so that not paying is a choice"); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _assertExactSettlementAndDelivery(before, commitmentExerciseAuthority, commitmentId, EXERCISE_Q, actualInput); + } + + /// @notice Proves the authoritative debt is what the pool charged, not what was requested or bounded. + /// @dev Three numbers that a wrong implementation would confuse: the quantity the request named, the + /// cost bound it carried, and the debt the swap produced. The exerciser pays the third, and it is + /// neither of the first two — an exact-output exercise for `q` costs more than `q` at a fee-bearing + /// pool moving against the buyer, and the bound here is the whole `uint256` domain. + function test_settlementAmount_isTheAuthoritativeDebtRatherThanTheRequest() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + assertGt(actualInput, EXERCISE_Q, "an exact-output exercise must cost more than the quantity it produces"); + assertLt(actualInput, UNCONSTRAINED_MAX_INPUT, "the cost bound must not be what is settled"); + + uint256 fundedBefore = ustb.balanceOf(commitmentExerciseAuthority); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + assertEq( + fundedBefore - ustb.balanceOf(commitmentExerciseAuthority), + actualInput, + "exactly the authoritative debt must be settled" + ); + } + + /*////////////////////////////////////////////////////////////// + G8C-5 — EXACT COST BOUND + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves a cost bound exactly equal to the authoritative debt is accepted. + /// @dev The boundary that must not be off by one in the safe direction. `maxInput` is the most the + /// exerciser is willing to pay, so paying exactly that much is within the bound. + function test_costBoundEqualToTheDebt_isAccepted() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q, actualInput); + + _assertExactSettlementAndDelivery(before, commitmentExerciseAuthority, commitmentId, EXERCISE_Q, actualInput); + } + + /// @notice Proves a cost bound one raw unit above the authoritative debt is accepted. + function test_costBoundAboveTheDebt_isAccepted() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q, actualInput + 1); + + _assertExactSettlementAndDelivery(before, commitmentExerciseAuthority, commitmentId, EXERCISE_Q, actualInput); + } + + /// @notice Proves a cost bound one raw unit below the authoritative debt unwinds the whole exercise. + /// @dev The refusal is reported against the debt production derived rather than against anything the + /// request supplied, and it takes the swap down with it: after it, the pool is exactly where it + /// was, nobody has paid, nobody has been delivered to, and no causal context survives that a later + /// attempt could build on. + function test_costBoundBelowTheDebt_unwindsTheWholeExercise() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + vm.expectRevert( + abi.encodeWithSelector( + ExerciseRouter.ExerciseRouter__ExerciseCostExceedsMaxInput.selector, actualInput, actualInput - 1 + ) + ); + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q, actualInput - 1); + + _assertNoExerciseResidue(before, commitmentExerciseAuthority, commitmentId); + } + + /*////////////////////////////////////////////////////////////// + G8C-6 — COST-PROTECTION ISOLATION + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves the cost bound leaves no trace in any authoritative Standby state. + /// @dev The tightest bound an exercise can carry is the exact debt, so if the field were going to reach + /// economic state anywhere this is the request that would show it. The causal context afterwards + /// carries exactly the seven bindings authorization resolved and no eighth, the commitment record + /// is untouched in every field, and both derived quantities are what the execution alone accounts + /// for — the bound changed the exercise's admissibility and nothing about its economics. + function test_costBound_leavesNoTraceInAuthoritativeState() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + StandbyHook.Commitment memory admitted = hook.commitment(commitmentId); + + uint256 predictedCapacity = + hook.prospectiveSupportingCapacityAfterSwap(_protectedExactOutputSwapParams(EXERCISE_Q)); + uint256 obligationBefore = hook.aggregateObligation(); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q, actualInput); + + _assertExercisedContext( + commitmentId, + commitmentExerciseAuthority, + beneficiary, + EXERCISE_Q, + "the cost bound must appear in no causal binding" + ); + _assertCommitmentFactsUnchanged(commitmentId, admitted, "the cost bound must reach no commitment fact"); + + assertEq(hook.aggregateObligation(), obligationBefore, "the cost bound must not move the obligation"); + assertEq(hook.supportingCapacity(), predictedCapacity, "capacity must be what the execution alone accounts for"); + } + + /*////////////////////////////////////////////////////////////// + G8C-11, G8C-12 — AUTHORITATIVE RECIPIENT + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves delivery follows the exercised commitment's own Beneficiary. + /// @dev Two commitments exist with different Beneficiaries and one exercise authority between them. The + /// request names only the commitment and the quantity, so the recipient is decided entirely by + /// which commitment was exercised — and the other Beneficiary receives nothing. + function test_delivery_followsTheExercisedCommitmentsBeneficiary() public { + (uint256 firstCommitmentId,) = _establishTwoBeneficiaries(); + + uint256 secondBeneficiaryBefore = usdc.balanceOf(secondBeneficiary); + uint256 beneficiaryBefore = usdc.balanceOf(beneficiary); + + _authorizeAs(commitmentExerciseAuthority, firstCommitmentId, EXERCISE_Q); + + assertEq( + usdc.balanceOf(beneficiary) - beneficiaryBefore, + EXERCISE_Q, + "the exercised commitment's Beneficiary must receive exactly q" + ); + assertEq(usdc.balanceOf(secondBeneficiary), secondBeneficiaryBefore, "no other Beneficiary may receive"); + } + + /// @notice Proves the same request against the other commitment delivers to the other Beneficiary. + /// @dev The complement, and the reason the first test is about the Beneficiary rather than about a + /// constant. Nothing in the request changes except which commitment it names. + function test_delivery_followsTheOtherCommitmentsBeneficiary() public { + (, uint256 secondCommitmentId) = _establishTwoBeneficiaries(); + + uint256 secondBeneficiaryBefore = usdc.balanceOf(secondBeneficiary); + uint256 beneficiaryBefore = usdc.balanceOf(beneficiary); + + _authorizeAs(commitmentExerciseAuthority, secondCommitmentId, EXERCISE_Q); + + assertEq( + usdc.balanceOf(secondBeneficiary) - secondBeneficiaryBefore, + EXERCISE_Q, + "the exercised commitment's Beneficiary must receive exactly q" + ); + assertEq(usdc.balanceOf(beneficiary), beneficiaryBefore, "no other Beneficiary may receive"); + } + + /*////////////////////////////////////////////////////////////// + G8C-16, G8C-17 — NO FULFILLMENT, NO NEW LIFECYCLE + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves a settled and delivered exercise still fulfils nothing. + /// @dev The whole F8C boundary in one assertion set. Value has moved in both directions and the + /// Beneficiary has been paid, and yet Remaining Entitlement, the admitted extent, the aggregate + /// obligation, and every other admitted commitment fact are exactly what they were — because + /// reducing them is finalization's, and finalization does not exist. + function test_settledAndDeliveredExercise_fulfilsNothing() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + StandbyHook.Commitment memory admitted = hook.commitment(commitmentId); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _assertNoFulfillmentConsequence( + before, commitmentExerciseAuthority, commitmentId, "settlement and delivery must fulfil nothing" + ); + _assertCommitmentFactsUnchanged(commitmentId, admitted, "settlement must touch no commitment fact"); + } + + /// @notice Proves the causal context is exactly where execution left it after settlement and delivery. + /// @dev No `SETTLED`, no `DELIVERED`, and no consumption: R4 resolves the deltas of the execution the + /// Hook already proved, and says nothing new about it. The bindings are still the ones + /// authorization resolved, which is what a later finalization must be able to rely on. + function test_causalContext_remainsExecutedAfterSettlementAndDelivery() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _assertExercisedContext( + commitmentId, + commitmentExerciseAuthority, + beneficiary, + EXERCISE_Q, + "settlement and delivery must leave the causal context exactly EXECUTED" + ); + } + + /*////////////////////////////////////////////////////////////// + G8C-15 — ONE RESOLUTION SEQUENCE + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves a settled exercise cannot be settled or delivered a second time. + /// @dev The second request is fully qualified on its own terms — the same authority, the same + /// commitment, a quantity well within the remainder, and a service that is still backed. It is + /// refused because the causal context of the first exercise is unresolved, and the Beneficiary is + /// paid exactly once. + function test_secondExerciseAfterSettlement_isRejected() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 beneficiaryBefore = usdc.balanceOf(beneficiary); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + vm.expectRevert(StandbyHook.StandbyHook__ExerciseAuthorizationAlreadyActive.selector); + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + assertEq(usdc.balanceOf(beneficiary) - beneficiaryBefore, EXERCISE_Q, "exactly one delivery may occur"); + } + + /*////////////////////////////////////////////////////////////// + G8C-18 — PAYMENT FAILURE ATOMICITY + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves an exerciser who cannot pay unwinds the whole exercise. + /// @dev The commitment is authentic, the authorization passes, and the swap executes — and then the + /// settlement transfer fails because the exerciser holds nothing. The router is holding far more + /// than the debt at that moment and none of it is used, which is the point: an exerciser who + /// cannot pay is a failed exercise, not a router-funded one. + function test_exerciserWithoutFunds_unwindsTheWholeExercise() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _drainExerciser(commitmentExerciseAuthority); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + assertGt(before.routerInput, actualInput, "the router must be holding more than the debt it must not pay"); + + vm.expectRevert( + abi.encodeWithSelector( + MockFixtureCurrency.MockFixtureCurrency__InsufficientBalance.selector, + commitmentExerciseAuthority, + 0, + actualInput + ) + ); + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _assertNoExerciseResidue(before, commitmentExerciseAuthority, commitmentId); + } + + /// @notice Proves an insufficient allowance unwinds the whole exercise. + /// @dev The exerciser is funded and the debt is affordable; only the amount the router was permitted to + /// coordinate is one raw unit short. Allowance is how the router coordinates a payment it never + /// holds, so an insufficient one is a failure to settle rather than an invitation to settle less. + function test_insufficientAllowance_unwindsTheWholeExercise() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _approveExerciser(commitmentExerciseAuthority, actualInput - 1); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + vm.expectRevert( + abi.encodeWithSelector( + MockFixtureCurrency.MockFixtureCurrency__InsufficientAllowance.selector, + commitmentExerciseAuthority, + address(configuredExerciseRouter), + actualInput - 1, + actualInput + ) + ); + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _assertNoExerciseResidue(before, commitmentExerciseAuthority, commitmentId); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Admits two backed commitments with different Beneficiaries under one exercise authority. + function _establishTwoBeneficiaries() internal returns (uint256 firstCommitmentId, uint256 secondCommitmentId) { + uint128 entitlement = uint128(EXERCISE_Q); + + firstCommitmentId = _establishExercisable(entitlement); + + secondCommitmentId = _establishAs( + establishmentAuthority, + secondBeneficiary, + commitmentExerciseAuthority, + entitlement, + uint64(block.timestamp), + uint64(block.timestamp) + DEFAULT_VALIDITY_DURATION + ); + } + + /// @dev Removes every unit of input currency an exerciser holds. + function _drainExerciser(address _exerciser) internal { + uint256 held = ustb.balanceOf(_exerciser); + + vm.prank(_exerciser); + bool drained = ustb.transfer(address(0xdead), held); + + assertTrue(drained, "the fixture must be able to remove the exerciser's funds"); + } +} + +/// @notice Integration evidence that the production exercise path stays fail-closed until finalization +/// exists (G8C-19). +/// @dev The only difference from the fixture above is the one that matters: the configured ExerciseRouter +/// is the production `ExerciseRouter`, with its completion barrier intact. Everything else is +/// identical — the same Hook, the same pool, the same liquidity, the same production commitment, the +/// same funded and approved exerciser, and the same request. +/// +/// What that isolates is the barrier itself. The exercise gets all the way through authorization, +/// execution, settlement, and delivery, and is then refused for the one thing that has not happened: +/// nothing consumed the causal proof. Because the proof is transaction-scoped, committing here would +/// leave a paid exerciser, a paid Beneficiary, an unreduced entitlement, and no surviving evidence +/// that any of it happened — so the whole exercise unwinds instead. +contract ProductionExerciseCompletionTest is BaseExerciseSettlementTest { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev The canonical commitment entitlement: 50,000 MockUSDC. + uint128 internal constant CANONICAL_ENTITLEMENT = uint128(StandbyFixtureConfig.CANONICAL_COMMITMENT_Q); + + /// @dev The exercise quantity used throughout: 20,000 MockUSDC. + uint256 internal constant EXERCISE_Q = 20_000_000_000; + + /*////////////////////////////////////////////////////////////// + G8C-19 — SLICE-COMPLETION SAFETY + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves a production exercise cannot commit while its causal proof is unconsumed. + /// @dev The refusal names the position the context was actually left in, which is what makes it a + /// completion condition rather than a placeholder: `EXECUTED` is exactly the state finalization + /// will consume, and consuming it is what will let this same requirement pass. + function test_productionExercise_cannotCompleteWhileUnfinalized() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + vm.expectRevert( + abi.encodeWithSelector( + ExerciseRouter.ExerciseRouter__ExerciseNotFinalized.selector, + StandbyHook.ExerciseAuthorizationState.EXECUTED + ) + ); + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _assertNoExerciseResidue(before, commitmentExerciseAuthority, commitmentId); + } + + /// @notice Proves the refusal survives every request shape a caller could try. + /// @dev A generous cost bound, an exact one, and a whole-remainder quantity are all refused identically. + /// The barrier is not about the request and cannot be routed around by making the request better. + function test_productionExercise_isRefusedWhateverTheRequest() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + _expectUnfinalizedRefusal(); + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q, UNCONSTRAINED_MAX_INPUT); + + _expectUnfinalizedRefusal(); + _authorizeAs(commitmentExerciseAuthority, commitmentId, CANONICAL_ENTITLEMENT, UNCONSTRAINED_MAX_INPUT); + + _expectUnfinalizedRefusal(); + _authorizeAs(commitmentExerciseAuthority, commitmentId, 1, UNCONSTRAINED_MAX_INPUT); + + _assertNoExerciseResidue(before, commitmentExerciseAuthority, commitmentId); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Activates the service with the production router, completion barrier intact. + function _resolveExerciseRouter() internal override returns (address router) { + configuredExerciseRouter = new ExerciseRouter(hook); + + router = address(configuredExerciseRouter); + } + + /// @dev Expects the production completion barrier's refusal of an unfinalized exercise. + function _expectUnfinalizedRefusal() internal { + vm.expectRevert( + abi.encodeWithSelector( + ExerciseRouter.ExerciseRouter__ExerciseNotFinalized.selector, + StandbyHook.ExerciseAuthorizationState.EXECUTED + ) + ); + } +} diff --git a/test/periphery/ExerciseSettlementPerimeter.t.sol b/test/periphery/ExerciseSettlementPerimeter.t.sol new file mode 100644 index 0000000..96c49a9 --- /dev/null +++ b/test/periphery/ExerciseSettlementPerimeter.t.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {StandbyFixtureConfig} from "../../script/helpers/StandbyFixtureConfig.sol"; + +import {ExerciseRouter} from "../../src/ExerciseRouter.sol"; +import {StandbyHook} from "../../src/StandbyHook.sol"; + +import {BaseExerciseSettlementTest} from "../shared/BaseExerciseSettlementTest.t.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Periphery evidence that the O2 coordinator acquires no economic authority by coordinating +/// settlement and delivery (G8C-7, G8C-11, G8C-13, G8C-14, G8C-15). +/// @dev Moving value is the point at which a coordinator is most likely to quietly become a principal. It +/// now pulls currency from an account and directs currency to another, and the question every test +/// below asks is the same one from a different angle: where did the authority for each of those come +/// from, and can anything outside the Hook supply it instead. +/// +/// The answers are structural rather than defensive. The payer and the recipient are read from the +/// Hook-owned causal context, the amounts are read from authoritative PoolManager accounting, and the +/// request surface carries no field through which either could be named — so there is no path to close +/// rather than a path that is checked. What is verified here is that those structures hold: that the +/// settlement entry point cannot be reached except through the PoolManager, that a fully funded router +/// is still not a payer, that an account other than the exercise authority cannot buy its way in by +/// funding one, and that a resolved exercise cannot be resolved again. +contract ExerciseSettlementPerimeterTest is BaseExerciseSettlementTest { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev The canonical commitment entitlement: 50,000 MockUSDC. + uint128 internal constant CANONICAL_ENTITLEMENT = uint128(StandbyFixtureConfig.CANONICAL_COMMITMENT_Q); + + /// @dev The exercise quantity used throughout: 20,000 MockUSDC. + uint256 internal constant EXERCISE_Q = 20_000_000_000; + + /*////////////////////////////////////////////////////////////// + G8C-14 — SETTLEMENT ENTRY AUTHORITY + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves the settlement callback cannot be entered by anyone but the PoolManager. + /// @dev The callback now carries the exerciser's own cost bound, and a caller who could invoke it + /// directly would be supplying that bound for someone else's exercise. It is refused before the + /// payload is used for anything, whoever the caller is and whatever bound the payload names. + function test_directSettlementCallback_isRefused() public { + _establishExercisable(CANONICAL_ENTITLEMENT); + + vm.expectRevert( + abi.encodeWithSelector(ExerciseRouter.ExerciseRouter__NotPoolManager.selector, unauthorizedExerciser) + ); + + vm.prank(unauthorizedExerciser); + configuredExerciseRouter.unlockCallback(abi.encode(type(uint256).max)); + } + + /// @notice Proves the settlement callback establishes nothing even for the commitment's own authority. + /// @dev The exercise authority is the one account that may exercise this commitment, and it still + /// cannot reach settlement except through the operation that produced a debt to settle. + function test_directSettlementCallback_isRefusedForTheExerciseAuthority() public { + _establishExercisable(CANONICAL_ENTITLEMENT); + + vm.expectRevert( + abi.encodeWithSelector(ExerciseRouter.ExerciseRouter__NotPoolManager.selector, commitmentExerciseAuthority) + ); + + vm.prank(commitmentExerciseAuthority); + configuredExerciseRouter.unlockCallback(abi.encode(type(uint256).max)); + } + + /*////////////////////////////////////////////////////////////// + G8C-7 — THE ROUTER IS NOT A PAYER + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves a router holding both currencies in quantity funds nothing. + /// @dev The router's balances are measured before and after a successful exercise. It could have paid + /// the whole debt many times over and its balance does not move by a single raw unit in either + /// currency, because the payer is a Hook-owned fact rather than whoever happens to have funds. + function test_fundedRouter_neitherFundsNorReceives() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + uint256 routerInputBefore = ustb.balanceOf(address(configuredExerciseRouter)); + uint256 routerOutputBefore = usdc.balanceOf(address(configuredExerciseRouter)); + + assertGt(routerInputBefore, actualInput, "the router must be able to cover the debt it must not pay"); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + assertEq(ustb.balanceOf(address(configuredExerciseRouter)), routerInputBefore, "the router is not the payer"); + assertEq( + usdc.balanceOf(address(configuredExerciseRouter)), + routerOutputBefore, + "the router takes no protected-output custody" + ); + } + + /// @notice Proves a third party cannot become the payer by funding and approving the router. + /// @dev The account is funded exactly as an exerciser would be and approves the router exactly as an + /// exerciser would. It is refused for the only reason that matters — it is not the commitment's + /// exercise authority — and its balance is untouched, so an outsider cannot buy into an exercise + /// by making itself able to pay for one. + function test_fundedThirdParty_cannotBecomeThePayer() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + _fundExerciser(unauthorizedExerciser, ACTOR_FUNDING); + _approveExerciser(unauthorizedExerciser, type(uint256).max); + + SettlementState memory before = _settlementState(unauthorizedExerciser, commitmentId); + + vm.expectRevert( + abi.encodeWithSelector( + StandbyHook.StandbyHook__NotCommitmentExerciseAuthority.selector, commitmentId, unauthorizedExerciser + ) + ); + _authorizeAs(unauthorizedExerciser, commitmentId, EXERCISE_Q); + + _assertNoExerciseResidue(before, unauthorizedExerciser, commitmentId); + } + + /// @notice Proves a third party's approval does not redirect an authorized exercise's payment. + /// @dev The complement: the outsider funds and approves, and then the real exercise authority exercises. + /// The debt still comes out of the authority's own account, because the payer is the authenticated + /// originating exerciser of this causal context and nobody else's willingness changes that. + function test_thirdPartyApproval_doesNotFundAnAuthorizedExercise() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + _fundExerciser(unauthorizedExerciser, ACTOR_FUNDING); + _approveExerciser(unauthorizedExerciser, type(uint256).max); + + uint256 actualInput = _probeActualInput(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + uint256 outsiderBefore = ustb.balanceOf(unauthorizedExerciser); + uint256 authorityBefore = ustb.balanceOf(commitmentExerciseAuthority); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + assertEq(ustb.balanceOf(unauthorizedExerciser), outsiderBefore, "a willing outsider must not be charged"); + assertEq( + authorityBefore - ustb.balanceOf(commitmentExerciseAuthority), + actualInput, + "the authenticated exerciser must fund its own exercise" + ); + } + + /*////////////////////////////////////////////////////////////// + G8C-11, G8C-13 — DELIVERY AUTHORITY + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves the delivered output comes from the PoolManager and lands only on the Beneficiary. + /// @dev Measured as a closed system: exactly `q` leaves the PoolManager, exactly `q` arrives at the + /// Beneficiary, and no other participant's protected-output balance moves at all. There is nowhere + /// the output could have been staged, and nothing left over to withdraw later. + function test_protectedOutput_movesOnlyFromThePoolManagerToTheBeneficiary() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + SettlementState memory before = _settlementState(commitmentExerciseAuthority, commitmentId); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + SettlementState memory current = _settlementState(commitmentExerciseAuthority, commitmentId); + + assertEq(before.poolManagerOutput - current.poolManagerOutput, EXERCISE_Q, "the output leaves the PoolManager"); + assertEq(current.beneficiaryOutput - before.beneficiaryOutput, EXERCISE_Q, "the Beneficiary receives it"); + assertEq(current.routerOutput, before.routerOutput, "the router is not an intermediary"); + assertEq(current.hookOutput, before.hookOutput, "the Hook is not an intermediary"); + assertEq(current.exerciserOutput, before.exerciserOutput, "the exerciser is not the recipient"); + } + + /*////////////////////////////////////////////////////////////// + G8C-15 — ONE RESOLUTION SEQUENCE + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves a resolved exercise cannot be resolved again through a second unlock. + /// @dev The router opens exactly one unlock per request, and a second request in the same transaction + /// is refused by the Hook before any unlock is attempted — so there is no second settlement, no + /// second delivery, and no way to re-enter the resolution sequence for a context that has already + /// been through it. + function test_resolvedExercise_cannotBeResolvedAgain() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + SettlementState memory resolved = _settlementState(commitmentExerciseAuthority, commitmentId); + + vm.expectRevert(StandbyHook.StandbyHook__ExerciseAuthorizationAlreadyActive.selector); + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + SettlementState memory current = _settlementState(commitmentExerciseAuthority, commitmentId); + + assertEq(current.exerciserInput, resolved.exerciserInput, "no second settlement may occur"); + assertEq(current.beneficiaryOutput, resolved.beneficiaryOutput, "no second delivery may occur"); + assertEq(current.poolManagerInput, resolved.poolManagerInput, "no second debt may be paid"); + assertEq(current.poolManagerOutput, resolved.poolManagerOutput, "no second credit may be discharged"); + } +} diff --git a/test/shared/BaseExerciseAuthorizationTest.t.sol b/test/shared/BaseExerciseAuthorizationTest.t.sol index 30073e1..1192465 100644 --- a/test/shared/BaseExerciseAuthorizationTest.t.sol +++ b/test/shared/BaseExerciseAuthorizationTest.t.sol @@ -33,13 +33,15 @@ import {BaseAuthenticBackingTest} from "./BaseAuthenticBackingTest.t.sol"; /// implementation. /// /// From F8B the configured router is `ExerciseDeltaClosureRouter` — the production `ExerciseRouter` -/// with mechanical PoolManager delta closure added and nothing else. It is needed because a completed -/// exercise now performs a real swap, and F8B implements no settlement, so no production path can -/// close the deltas that swap opens and no exercise could otherwise commit. Every authorization -/// predicate below is still decided by the production `authorizeExercise` against production state, -/// and every rejection below still occurs inside it, before any unlock. The closure contributes no -/// economics: it assigns no payer, enforces no cost bound, delivers nothing to the Beneficiary, and -/// attributes no fulfillment. +/// with mechanical PoolManager delta closure in place of settlement, and nothing else. It is what +/// keeps the F8A and F8B evidence about what it is about: a completed exercise performs a real swap, +/// and these suites are about the authorization that admitted it and the Hook-owned evidence that it +/// happened, neither of which may quietly come to depend on the settlement and delivery that follow. +/// Every authorization predicate below is still decided by the production `authorizeExercise` against +/// production state, and every rejection below still occurs inside it, before any unlock. The closure +/// contributes no economics: it assigns no payer, enforces no cost bound, delivers nothing to the +/// Beneficiary, and attributes no fulfillment. The settlement and delivery those are the absence of +/// are exercised on the real production path by `BaseExerciseSettlementTest`. /// /// Roles stay separate throughout. The commitment's exercise authority is not the Beneficiary, not the /// establishment authority, not the configuration authority, not a trader, not a liquidity provider, @@ -61,10 +63,10 @@ abstract contract BaseExerciseAuthorizationTest is BaseAuthenticBackingTest { /// @dev The `maxInput` a request carries when the test is not about `maxInput`. /// - /// Any value would do, which is the point: nothing implemented so far reads or forwards this - /// field, so no test outcome may depend on which value it is. Enforcing it against the input debt - /// the executed swap produces belongs to F8C. The suites that are about the field say so - /// explicitly and vary it across the `uint256` domain. + /// The point is that it constrains nothing. It is the largest bound expressible, so no input debt + /// the pool could ever produce can breach it and no outcome below can turn on the field. The + /// suites that are about the field say so explicitly and choose their bounds against the + /// authoritative debt the executed swap actually produced. uint256 internal constant UNCONSTRAINED_MAX_INPUT = type(uint256).max; /*////////////////////////////////////////////////////////////// diff --git a/test/shared/BaseExerciseSettlementTest.t.sol b/test/shared/BaseExerciseSettlementTest.t.sol new file mode 100644 index 0000000..9963415 --- /dev/null +++ b/test/shared/BaseExerciseSettlementTest.t.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {ExerciseRouter} from "../../src/ExerciseRouter.sol"; +import {StandbyHook} from "../../src/StandbyHook.sol"; + +import {UnfinalizedExerciseRouter} from "../harness/UnfinalizedExerciseRouter.sol"; +import {BaseExerciseAuthorizationTest} from "./BaseExerciseAuthorizationTest.t.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Shared real-path fixture for F8C settlement and Beneficiary-delivery evidence. +/// @dev Built on the F8A/F8B fixture without altering it: the same real pinned `PoolManager`, the same +/// canonical `StandbyHook` deployment, the same F1 currencies, the same F2 registry under its own +/// administrator, the same production activation, the same canonical liquidity added through the +/// production `beforeAddLiquidity` path, and the same production `establishCommitment` transition for +/// every commitment. Nothing economic is seeded. +/// +/// Two things differ from the F8B layer, and both are about being able to observe production R4 at +/// all. +/// +/// The configured ExerciseRouter is `UnfinalizedExerciseRouter` rather than the F8B delta-closure +/// router. That is the production `ExerciseRouter` with its completion barrier lifted and nothing else +/// changed: the input-debt derivation, the `maxInput` comparison, the exerciser-funded settlement, and +/// the direct Beneficiary delivery are all production code running unmodified against the real +/// PoolManager. The barrier has to be lifted because production deliberately refuses to return from an +/// exercise whose causal proof has not been consumed, and nothing at this slice can consume it — so +/// every production exercise unwinds before its own settlement could be measured. Lifting it proves +/// nothing about whether a production O2 completes, and the suites below never claim it does; the +/// production barrier itself is verified separately, against the production router. +/// +/// And the exerciser is funded and approves the router, because the exerciser is now the payer. The +/// funding is input currency only: an exerciser holding none of the protected output currency cannot +/// be the source of anything the Beneficiary receives. +/// +/// What is deliberately *not* narrowed is the router's own pre-funding, inherited from the F8B layer. +/// A router holding a large balance of both currencies while every settlement still comes out of the +/// exerciser's account is what makes "the router could have paid and did not" a checkable claim rather +/// than an absence. +/// +/// Roles stay separate throughout. The exerciser is not the Beneficiary, so a delivery that landed +/// with the payer would be visible; neither is the router, the Hook, the establishment authority, the +/// configuration authority, a trader, a liquidity provider, or the registry administrator. +abstract contract BaseExerciseSettlementTest is BaseExerciseAuthorizationTest { + /*////////////////////////////////////////////////////////////// + TYPE DECLARATIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Everything an exercise moves, and everything it must leave exactly as it found it. + /// @dev Deliberately spans every account a settlement or a delivery could land in, both currencies, and + /// the authoritative economic facts F8C must not touch. Checking only the Beneficiary would miss + /// output that also passed through the router; checking only Remaining Entitlement would miss an + /// obligation released early. + /// @param exerciserInput The authenticated exerciser's input-currency balance. + /// @param exerciserOutput The authenticated exerciser's protected-output balance. + /// @param beneficiaryOutput The authoritative Beneficiary's protected-output balance. + /// @param routerInput The configured ExerciseRouter's input-currency balance. + /// @param routerOutput The configured ExerciseRouter's protected-output balance. + /// @param hookInput The Hook's input-currency balance. + /// @param hookOutput The Hook's protected-output balance. + /// @param poolManagerInput The PoolManager's input-currency balance. + /// @param poolManagerOutput The PoolManager's protected-output balance. + /// @param remainingEntitlement The exercised commitment's authoritative Remaining Entitlement. + /// @param originalEntitlement The exercised commitment's admitted entitlement extent. + /// @param aggregateObligation The re-derived Aggregate Capacity Obligation. + /// @param sqrtPriceX96 The authoritative pool square-root price. + /// @param tick The authoritative pool tick. + /// @param liquidity The authoritative active liquidity. + struct SettlementState { + uint256 exerciserInput; + uint256 exerciserOutput; + uint256 beneficiaryOutput; + uint256 routerInput; + uint256 routerOutput; + uint256 hookInput; + uint256 hookOutput; + uint256 poolManagerInput; + uint256 poolManagerOutput; + uint128 remainingEntitlement; + uint128 originalEntitlement; + uint256 aggregateObligation; + uint160 sqrtPriceX96; + int24 tick; + uint128 liquidity; + } + + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev The configured router, typed so the production derivation pass-through is reachable. + UnfinalizedExerciseRouter internal settlementExerciseRouter; + + /*////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + + /// @notice Makes the canonical exercise authority an exerciser that can actually pay. + function setUp() public virtual override { + super.setUp(); + + _fundExerciser(commitmentExerciseAuthority, ACTOR_FUNDING); + _approveExerciser(commitmentExerciseAuthority, type(uint256).max); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Activates the service with the production router whose completion barrier is lifted. + function _resolveExerciseRouter() internal virtual override returns (address router) { + settlementExerciseRouter = new UnfinalizedExerciseRouter(hook); + + configuredExerciseRouter = settlementExerciseRouter; + + router = address(configuredExerciseRouter); + } + + /// @dev Gives an exerciser input currency, and only input currency. + function _fundExerciser(address _exerciser, uint256 _amount) internal { + ustb.mint(_exerciser, _amount); + } + + /// @dev Lets the configured router coordinate an exerciser's payment, up to a chosen bound. + function _approveExerciser(address _exerciser, uint256 _allowance) internal { + vm.prank(_exerciser); + ustb.approve(address(configuredExerciseRouter), _allowance); + } + + /// @dev Discovers the authoritative input debt an exercise would produce, from production itself. + /// + /// The only authority on what an exercise costs is the swap the PoolManager performs, so this asks + /// production what that swap cost — by requesting the exercise under a cost bound of zero, which no + /// real debt can satisfy, and reading the debt out of the refusal production reports. The refusal + /// unwinds the swap along with everything else, so the pool is exactly where it was and the same + /// request will produce the same debt again. + /// + /// That is what makes the `maxInput` boundaries testable as boundaries. A bound derived from a + /// quote, an estimate, or an arithmetic reconstruction would be a bound on a different number than + /// the one production compares against, and equality against the wrong number proves nothing. + function _probeActualInput(address _exerciser, uint256 _commitmentId, uint256 _q) + internal + returns (uint256 actualInput) + { + vm.prank(_exerciser); + + try configuredExerciseRouter.exercise(_commitmentId, _q, 0) { + revert("a zero cost bound must refuse every real exercise"); + } catch (bytes memory reason) { + actualInput = _decodeCostBreach(reason); + } + } + + /// @dev Reads the authoritative input debt out of production's own cost-bound refusal. + function _decodeCostBreach(bytes memory _reason) internal pure returns (uint256 actualInput) { + if (bytes4(_reason) != ExerciseRouter.ExerciseRouter__ExerciseCostExceedsMaxInput.selector) { + revert("the exercise must have been refused by its cost bound"); + } + + bytes memory payload = new bytes(_reason.length - 4); + + for (uint256 i = 0; i < payload.length; ++i) { + payload[i] = _reason[i + 4]; + } + + (actualInput,) = abi.decode(payload, (uint256, uint256)); + } + + /// @dev Captures every balance and authoritative fact an exercise could move. + /// + /// The obligation is re-derived rather than remembered from an earlier call, so a comparison is + /// between two authoritative derivations rather than between a derivation and a cached number. + function _settlementState(address _exerciser, uint256 _commitmentId) + internal + view + returns (SettlementState memory state) + { + (uint160 sqrtPriceX96, int24 tick, uint128 liquidity) = _servicePoolState(); + + StandbyHook.Commitment memory record = hook.commitment(_commitmentId); + + state = SettlementState({ + exerciserInput: ustb.balanceOf(_exerciser), + exerciserOutput: usdc.balanceOf(_exerciser), + beneficiaryOutput: usdc.balanceOf(record.beneficiary), + routerInput: ustb.balanceOf(address(configuredExerciseRouter)), + routerOutput: usdc.balanceOf(address(configuredExerciseRouter)), + hookInput: ustb.balanceOf(address(hook)), + hookOutput: usdc.balanceOf(address(hook)), + poolManagerInput: ustb.balanceOf(address(poolManager)), + poolManagerOutput: usdc.balanceOf(address(poolManager)), + remainingEntitlement: record.remainingEntitlement, + originalEntitlement: record.originalEntitlement, + aggregateObligation: hook.aggregateObligation(), + sqrtPriceX96: sqrtPriceX96, + tick: tick, + liquidity: liquidity + }); + } + + /// @dev Proves a successful exercise moved exactly what it must and nothing else. + /// + /// Four independent statements, each of which a wrong settlement would break on its own: the + /// exerciser paid the exact debt, the PoolManager received exactly that debt, the Beneficiary + /// received exactly the authorized quantity, and it came out of the PoolManager rather than out of + /// anyone's custody. The router and the Hook are checked on both currencies, because a coordinator + /// that briefly held either would be a custodian. + function _assertExactSettlementAndDelivery( + SettlementState memory _before, + address _exerciser, + uint256 _commitmentId, + uint256 _q, + uint256 _actualInput + ) internal view { + SettlementState memory current = _settlementState(_exerciser, _commitmentId); + + assertEq( + _before.exerciserInput - current.exerciserInput, + _actualInput, + "the authenticated exerciser must fund exactly the authoritative input debt" + ); + assertEq( + current.poolManagerInput - _before.poolManagerInput, + _actualInput, + "exactly that debt must reach the PoolManager" + ); + assertEq( + current.beneficiaryOutput - _before.beneficiaryOutput, + _q, + "the authoritative Beneficiary must receive exactly the authorized quantity" + ); + assertEq( + _before.poolManagerOutput - current.poolManagerOutput, + _q, + "exactly that quantity must leave the PoolManager" + ); + + assertEq(current.exerciserOutput, _before.exerciserOutput, "the exerciser must receive no protected output"); + assertEq(current.routerInput, _before.routerInput, "the router must not fund settlement"); + assertEq(current.routerOutput, _before.routerOutput, "the router must take no protected-output custody"); + assertEq(current.hookInput, _before.hookInput, "the Hook must not fund settlement"); + assertEq(current.hookOutput, _before.hookOutput, "the Hook must take no protected-output custody"); + } + + /// @dev Proves settlement and delivery changed no Standby economic fact. + /// + /// Everything finalization owns and F8C must leave untouched: the entitlement, its remainder, and + /// the aggregate obligation. A settled and delivered exercise is still an unfulfilled one. + function _assertNoFulfillmentConsequence( + SettlementState memory _before, + address _exerciser, + uint256 _commitmentId, + string memory _context + ) internal view { + SettlementState memory current = _settlementState(_exerciser, _commitmentId); + + assertEq(uint256(current.remainingEntitlement), uint256(_before.remainingEntitlement), _context); + assertEq(uint256(current.originalEntitlement), uint256(_before.originalEntitlement), _context); + assertEq(current.aggregateObligation, _before.aggregateObligation, _context); + } + + /// @dev Proves a rejected exercise left no residue of any kind, on any surface. + function _assertNoExerciseResidue(SettlementState memory _before, address _exerciser, uint256 _commitmentId) + internal + view + { + SettlementState memory current = _settlementState(_exerciser, _commitmentId); + + assertEq(current.exerciserInput, _before.exerciserInput, "a rejected exercise must not take payment"); + assertEq(current.exerciserOutput, _before.exerciserOutput, "a rejected exercise must not pay the exerciser"); + assertEq(current.beneficiaryOutput, _before.beneficiaryOutput, "a rejected exercise must deliver nothing"); + assertEq(current.routerInput, _before.routerInput, "a rejected exercise must not move router currency0"); + assertEq(current.routerOutput, _before.routerOutput, "a rejected exercise must not move router currency1"); + assertEq(current.hookInput, _before.hookInput, "a rejected exercise must not move Hook currency0"); + assertEq(current.hookOutput, _before.hookOutput, "a rejected exercise must not move Hook currency1"); + assertEq(current.poolManagerInput, _before.poolManagerInput, "a rejected exercise must not settle input"); + assertEq(current.poolManagerOutput, _before.poolManagerOutput, "a rejected exercise must not release output"); + assertEq( + uint256(current.remainingEntitlement), + uint256(_before.remainingEntitlement), + "a rejected exercise must fulfil nothing" + ); + assertEq( + uint256(current.originalEntitlement), + uint256(_before.originalEntitlement), + "a rejected exercise must change no admitted term" + ); + assertEq( + current.aggregateObligation, _before.aggregateObligation, "a rejected exercise must release no obligation" + ); + assertEq(current.sqrtPriceX96, _before.sqrtPriceX96, "a rejected exercise must leave the pool price untouched"); + assertEq(current.tick, _before.tick, "a rejected exercise must leave the pool tick untouched"); + assertEq(current.liquidity, _before.liquidity, "a rejected exercise must leave active liquidity untouched"); + + _assertNoAuthorizationContext("a rejected exercise must leave no reusable causal context"); + } +} diff --git a/test/unit/ExerciseSettlementDerivation.t.sol b/test/unit/ExerciseSettlementDerivation.t.sol new file mode 100644 index 0000000..423fc6b --- /dev/null +++ b/test/unit/ExerciseSettlementDerivation.t.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {BalanceDelta, toBalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {SwapParams} from "v4-core/types/PoolOperation.sol"; + +import {StandbyFixtureConfig} from "../../script/helpers/StandbyFixtureConfig.sol"; + +import {ExerciseRouter} from "../../src/ExerciseRouter.sol"; +import {StandbyHook} from "../../src/StandbyHook.sol"; + +import {UnfinalizedExerciseRouter} from "../harness/UnfinalizedExerciseRouter.sol"; +import {BaseExerciseSettlementTest} from "../shared/BaseExerciseSettlementTest.t.sol"; +import {BaseUnbackedExerciseAuthorizationTest} from "../shared/BaseUnbackedExerciseAuthorizationTest.t.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Unit evidence for the authoritative settlement derivation of an executed protected swap +/// (G8C-1 through G8C-4). +/// @dev The derivation answers three questions about one executed operation: which currency it spends, +/// which currency it produces, and how much it owes. All three come from the operation itself and the +/// `BalanceDelta` the PoolManager produced for it, and getting any of them wrong would settle the +/// wrong currency, deliver the wrong currency, or pay the wrong amount — while every surrounding +/// check still passed. +/// +/// The real PoolManager cannot produce the cases that matter here. It will only ever hand back a +/// well-formed delta for a swap it actually performed, so a wrong-signed input side, a zero input +/// side, and a delta whose negative amount is on the output side are all unreachable in production — +/// which is precisely why the derivation must refuse them and why refusing them is verifiable only +/// through the harness pass-through. Everything below is the production derivation running unmodified. +contract ExerciseSettlementDerivationTest is BaseExerciseSettlementTest { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev A plausible protected-output amount: 20,000 MockUSDC. + int128 internal constant PLAUSIBLE_OUTPUT = 20_000_000_000; + + /// @dev The debt a plausible protected execution of that quantity would owe. + int128 internal constant PLAUSIBLE_DEBT = -20_050_000_000; + + /*////////////////////////////////////////////////////////////// + G8C-1 — AUTHORITATIVE CURRENCIES + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves a protected `zeroForOne` execution spends currency0 and produces currency1. + function test_protectedDirection_spendsCurrency0AndProducesCurrency1() public view { + (Currency inputCurrency, Currency outputCurrency, uint256 actualInput) = settlementExerciseRouter + .executedExerciseSettlement( + servicePoolKey, _protectedExactOutputSwapParams(uint256(uint128(PLAUSIBLE_OUTPUT))), _delta() + ); + + assertEq(Currency.unwrap(inputCurrency), address(ustb), "currency0 is spent in the protected direction"); + assertEq(Currency.unwrap(outputCurrency), address(usdc), "currency1 is produced in the protected direction"); + assertEq(actualInput, uint256(uint128(-PLAUSIBLE_DEBT)), "the debt is the currency0 side of the delta"); + } + + /// @notice Proves a `oneForZero` execution spends currency1 and produces currency0. + /// @dev The mirror, on the same delta. Only the direction of the executed operation changes, and it + /// changes which side of the delta is the debt as well as which currency each side names — so a + /// derivation that had hardcoded the canonical fixture's direction would report the output side as + /// the amount owed and settle it in the wrong currency. + function test_oppositeDirection_spendsCurrency1AndProducesCurrency0() public view { + (Currency inputCurrency, Currency outputCurrency, uint256 actualInput) = settlementExerciseRouter + .executedExerciseSettlement( + servicePoolKey, _oppositeExactOutputSwapParams(uint256(uint128(PLAUSIBLE_OUTPUT))), _mirroredDelta() + ); + + assertEq(Currency.unwrap(inputCurrency), address(usdc), "currency1 is spent in the opposite direction"); + assertEq(Currency.unwrap(outputCurrency), address(ustb), "currency0 is produced in the opposite direction"); + assertEq(actualInput, uint256(uint128(-PLAUSIBLE_DEBT)), "the debt is the currency1 side of the delta"); + } + + /*////////////////////////////////////////////////////////////// + G8C-2, G8C-3 — SIGNED INTERPRETATION + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves a credit-signed input side is refused rather than settled. + /// @dev A positive amount is currency the PoolManager owes, and there is no sense in which it is an + /// amount to be paid. Taking its magnitude would produce a plausible-looking settlement out of a + /// delta that says the opposite of what settlement assumes. + function test_creditSignedInputSide_isRefused() public { + vm.expectRevert( + abi.encodeWithSelector( + ExerciseRouter.ExerciseRouter__NoAuthoritativeInputDebt.selector, int256(PLAUSIBLE_OUTPUT) + ) + ); + settlementExerciseRouter.executedExerciseSettlement( + servicePoolKey, + _protectedExactOutputSwapParams(uint256(uint128(PLAUSIBLE_OUTPUT))), + toBalanceDelta(PLAUSIBLE_OUTPUT, PLAUSIBLE_OUTPUT) + ); + } + + /// @notice Proves a zero input side is refused rather than settled as a free exercise. + function test_zeroInputSide_isRefused() public { + vm.expectRevert( + abi.encodeWithSelector(ExerciseRouter.ExerciseRouter__NoAuthoritativeInputDebt.selector, int256(0)) + ); + settlementExerciseRouter.executedExerciseSettlement( + servicePoolKey, + _protectedExactOutputSwapParams(uint256(uint128(PLAUSIBLE_OUTPUT))), + toBalanceDelta(0, PLAUSIBLE_OUTPUT) + ); + } + + /// @notice Proves the debt is read from the executed direction's input side, not from whichever side + /// happens to be negative. + /// @dev The delta here is the canonical one with its halves exchanged: the debt sits on the protected + /// output side and the credit on the input side. A derivation that searched for a negative amount + /// would find one, settle the output currency, and be wrong twice; this one refuses, because the + /// side it must read is decided by the operation and not by the values. + function test_debtOnTheWrongSide_isRefused() public { + vm.expectRevert( + abi.encodeWithSelector( + ExerciseRouter.ExerciseRouter__NoAuthoritativeInputDebt.selector, int256(PLAUSIBLE_OUTPUT) + ) + ); + settlementExerciseRouter.executedExerciseSettlement( + servicePoolKey, + _protectedExactOutputSwapParams(uint256(uint128(PLAUSIBLE_OUTPUT))), + toBalanceDelta(PLAUSIBLE_OUTPUT, PLAUSIBLE_DEBT) + ); + } + + /*////////////////////////////////////////////////////////////// + G8C-4 — SAFE CONVERSION + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves the extreme of the signed delta domain converts exactly rather than wrapping. + /// @dev `type(int128).min` is the one value whose negation is not representable in its own type, and + /// negating it in place would wrap back to itself — turning the largest debt the PoolManager can + /// express into a nonsensical magnitude. Widening before negating converts it exactly. + function test_minimumSignedDebt_convertsExactly() public view { + (,, uint256 actualInput) = settlementExerciseRouter.executedExerciseSettlement( + servicePoolKey, + _protectedExactOutputSwapParams(uint256(uint128(PLAUSIBLE_OUTPUT))), + toBalanceDelta(type(int128).min, PLAUSIBLE_OUTPUT) + ); + + assertEq(actualInput, 2 ** 127, "the minimum signed delta must convert to its exact magnitude"); + } + + /// @notice Proves the smallest expressible debt is a debt. + /// @dev One raw unit is not nothing, and the boundary between "refused" and "settled" is exactly at + /// zero rather than near it. + function test_smallestSignedDebt_isSettleable() public view { + (,, uint256 actualInput) = settlementExerciseRouter.executedExerciseSettlement( + servicePoolKey, + _protectedExactOutputSwapParams(uint256(uint128(PLAUSIBLE_OUTPUT))), + toBalanceDelta(-1, PLAUSIBLE_OUTPUT) + ); + + assertEq(actualInput, 1, "one raw unit of debt must be settleable"); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev The delta a plausible protected execution would produce: currency0 owed, currency1 received. + function _delta() internal pure returns (BalanceDelta delta) { + delta = toBalanceDelta(PLAUSIBLE_DEBT, PLAUSIBLE_OUTPUT); + } + + /// @dev The same delta for the opposite direction: currency1 owed, currency0 received. + function _mirroredDelta() internal pure returns (BalanceDelta delta) { + delta = toBalanceDelta(PLAUSIBLE_OUTPUT, PLAUSIBLE_DEBT); + } + + /// @dev Builds an opposite-direction exact-output swap confined to the service domain. + function _oppositeExactOutputSwapParams(uint256 _amountOut) internal pure returns (SwapParams memory params) { + params = _swapParams(false, int256(_amountOut), StandbyFixtureConfig.TICK_O); + } +} + +/// @notice Unit evidence that settlement and delivery require the Hook's own execution proof (G8C-14). +/// @dev Resolution moves an exerciser's currency and pays a Beneficiary, and the only authority that any of +/// that is owed is the Hook-owned causal context reaching `EXECUTED`. In production that position is +/// the only one resolution can ever encounter, because it runs immediately after a swap the Hook +/// accepted and proved — which is exactly why the refusal of every other position is unreachable there +/// and has to be presented directly. +/// +/// The router under test is a fresh production router rather than the configured one, and it is +/// deliberately not the coordinator this service was activated with. It still refuses, because what +/// authorizes resolution is the causal position rather than the identity of whoever asks. +contract ExerciseSettlementCausalPrerequisiteTest is BaseUnbackedExerciseAuthorizationTest { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev The authorized quantity every seeded context carries: 20,000 MockUSDC. + uint256 internal constant SEEDED_Q = 20_000_000_000; + + /// @dev The debt a plausible protected execution of that quantity would owe. + int128 internal constant PLAUSIBLE_DEBT = -20_050_000_000; + + /// @dev A production router bound to this fixture's Hook, with its resolution reachable directly. + UnfinalizedExerciseRouter internal resolvingRouter; + + /*////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + + /// @notice Adds a directly reachable production resolution path to the harness fixture. + function setUp() public virtual override { + super.setUp(); + + resolvingRouter = new UnfinalizedExerciseRouter(hook); + } + + /*////////////////////////////////////////////////////////////// + G8C-14 — EXECUTION PROOF REQUIRED + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves an absent causal context authorizes no settlement and no delivery. + function test_absentContext_authorizesNoResolution() public { + _expectResolutionRefusal(StandbyHook.ExerciseAuthorizationState.EMPTY); + + _resolve(); + } + + /// @notice Proves an authorization that has not executed authorizes no settlement and no delivery. + /// @dev The complete bindings are present — this commitment, this exerciser, this Beneficiary, this + /// quantity — and every one of them is what a resolution would need. What is missing is the only + /// thing that makes a debt exist: proof that the swap happened. + function test_authorizedButUnexecutedContext_authorizesNoResolution() public { + _seedContext(StandbyHook.ExerciseAuthorizationState.AUTHORIZED); + + _expectResolutionRefusal(StandbyHook.ExerciseAuthorizationState.AUTHORIZED); + + _resolve(); + } + + /// @notice Proves a swap that is merely in flight authorizes no settlement and no delivery. + /// @dev `EXECUTING` means the Hook accepted the proposal, not that the pool produced anything. Settling + /// against it would pay for an execution whose actual result has not been seen. + function test_inFlightContext_authorizesNoResolution() public { + _seedContext(StandbyHook.ExerciseAuthorizationState.EXECUTING); + + _expectResolutionRefusal(StandbyHook.ExerciseAuthorizationState.EXECUTING); + + _resolve(); + } + + /// @notice Proves an in-flight authorization decision authorizes no settlement and no delivery. + function test_undecidedContext_authorizesNoResolution() public { + _seedContext(StandbyHook.ExerciseAuthorizationState.AUTHORIZING); + + _expectResolutionRefusal(StandbyHook.ExerciseAuthorizationState.AUTHORIZING); + + _resolve(); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Presents a plausible executed exercise to the production resolution. + function _resolve() internal { + resolvingRouter.resolveExecutedExercise( + servicePoolKey, + _protectedExactOutputSwapParams(SEEDED_Q), + toBalanceDelta(PLAUSIBLE_DEBT, int128(int256(SEEDED_Q))), + type(uint256).max + ); + } + + /// @dev Expects the refusal of a resolution offered against a causal position that proves nothing. + function _expectResolutionRefusal(StandbyHook.ExerciseAuthorizationState _state) internal { + vm.expectRevert(abi.encodeWithSelector(ExerciseRouter.ExerciseRouter__ExerciseNotExecuted.selector, _state)); + } + + /// @dev Writes a complete causal context at a chosen position, with authentic-looking bindings. + function _seedContext(StandbyHook.ExerciseAuthorizationState _state) internal { + serviceHarness.writeExerciseAuthorization( + StandbyHook.ExerciseAuthorizationContext({ + state: _state, + serviceId: servicePoolId, + commitmentId: 1, + exerciseRouter: exerciseRouter, + exerciser: commitmentExerciseAuthority, + beneficiary: beneficiary, + q: SEEDED_Q + }) + ); + } +}