From f0eba1d59b7b0c32d6507fe6f40b87a0fc3c37e3 Mon Sep 17 00:00:00 2001 From: garykocsis Date: Mon, 7 Sep 2026 23:47:24 -0400 Subject: [PATCH 1/2] feat: implement F8B protected execution evidence --- docs/project-status.md | 46 +- ...session-12-f8b-documentation-correction.md | 212 +++ ...ession-12-f8b-o2-exact-output-execution.md | 1364 +++++++++++++++++ docs/prompts/session-12-log.md | 410 +++++ src/ExerciseRouter.sol | 114 +- src/StandbyHook.sol | 362 ++++- test/fuzz/ExerciseAuthorizationFuzz.t.sol | 19 +- test/fuzz/ExerciseBackingFuzz.t.sol | 33 +- test/fuzz/ProtectedExecutionFuzz.t.sol | 226 +++ test/harness/AdversarialExerciseRouter.sol | 272 ++++ test/harness/ExerciseDeltaClosureRouter.sol | 104 ++ test/harness/StandbyHookHarness.sol | 53 +- test/integration/ExerciseAuthorization.t.sol | 82 +- test/integration/ProtectedExecution.t.sol | 348 +++++ .../ExerciseAuthorizationPerimeter.t.sol | 15 +- .../ProtectedExecutionPerimeter.t.sol | 314 ++++ test/shared/BaseAdversarialExerciseTest.t.sol | 49 + .../BaseExerciseAuthorizationTest.t.sol | 91 +- test/unit/ExerciseAuthorization.t.sol | 4 +- test/unit/ProtectedExecutionEvidence.t.sol | 429 ++++++ 20 files changed, 4360 insertions(+), 187 deletions(-) create mode 100644 docs/prompts/session-12-f8b-documentation-correction.md create mode 100644 docs/prompts/session-12-f8b-o2-exact-output-execution.md create mode 100644 docs/prompts/session-12-log.md create mode 100644 test/fuzz/ProtectedExecutionFuzz.t.sol create mode 100644 test/harness/AdversarialExerciseRouter.sol create mode 100644 test/harness/ExerciseDeltaClosureRouter.sol create mode 100644 test/integration/ProtectedExecution.t.sol create mode 100644 test/periphery/ProtectedExecutionPerimeter.t.sol create mode 100644 test/shared/BaseAdversarialExerciseTest.t.sol create mode 100644 test/unit/ProtectedExecutionEvidence.t.sol diff --git a/docs/project-status.md b/docs/project-status.md index 72bfbc3..c609a9a 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:** F8B — O2 Exact-Output Execution / Execution Evidence (NEXT AUTHORIZED / NOT STARTED) -**Last Closed Gate:** G8A — Authorization / Hook-Owned Causal Context 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 NOT STARTED +**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 --- @@ -41,15 +41,15 @@ Implementation proceeds through the verification-gated F0–F10 ladder defined i The next authorized slice is: -> **F8B — O2 Exact-Output Execution / Execution Evidence** +> **F8C — Authoritative Settlement / Direct Beneficiary Delivery** The last closed gate is: -> **G8A — Authorization / Hook-Owned Causal Context Gate** +> **G8B — O2 Exact-Output Execution / Execution-Evidence Gate** -F0, F1, F2, F3, F4, F5, F6A, F7, F6B and F8A 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 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. -F8A was reviewed independently and G8A was explicitly closed. F8B is the next authorized implementation slice and has not been started. +F8B was reviewed independently and G8B was explicitly closed. F8C is the next authorized implementation slice and has not been started. --- @@ -69,8 +69,8 @@ Current implementation sequence: | F7 | O1 Commitment Establishment | **COMPLETE — G7 CLOSED** | | F6B | O3 Enforcement with Authentic O > 0 | **COMPLETE — G6B CLOSED** | | F8A | O2 Authorization | **COMPLETE — G8A CLOSED** | -| F8B | O2 Exact-Output Execution + Causal Evidence | **NEXT / NOT STARTED** | -| F8C | O2 Input Settlement + Direct Beneficiary Delivery | NOT STARTED | +| 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 | | GI | Full Stateful Invariant Verification | NOT STARTED | | F9 | Canonical Acceptance | NOT STARTED | @@ -217,7 +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. -O2 execution, settlement, delivery and finalization remain unimplemented. +F8B adds the production O2 execution classification and execution evidence. Settlement, delivery and +finalization remain unimplemented. ### `script/DeployStandbyHook.s.sol` @@ -370,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 or F8A implementation responsibility and no open gate. +There is no unresolved F0, F1, F2, F3, F4, F5, F6A, F7, F6B, F8A or F8B implementation responsibility and no open gate. The next unstarted responsibility is: -> **F8B — O2 Exact-Output Execution / Execution Evidence.** +> **F8C — Authoritative Settlement / Direct Beneficiary Delivery.** -F8B is authorized as the next implementation slice but has not been started. No further downstream slice — -F8C or F8D — is authorized. +F8C is authorized as the next implementation slice but has not been started. No further downstream slice — +F8D — is authorized. Known limitations carried forward from F0, none of which blocked G0: @@ -388,7 +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 execution, settlement, delivery and finalization remain + before its service is activated, and O2 settlement, delivery and finalization remain unimplemented. --- @@ -581,11 +582,11 @@ The root `CLAUDE.md` will define the repository operating rules and document aut ## 18. Next Action -G8A has been reviewed and closed, so the next action is: +G8B has been reviewed and closed, so the next action is: -> **F8B — O2 Exact-Output Execution / Execution Evidence.** +> **F8C — Authoritative Settlement / Direct Beneficiary Delivery.** -F8B may begin when explicitly tasked. Its own verification gate governs advancement beyond the F8B boundary. +F8C may begin when explicitly tasked. Its own verification gate governs advancement beyond the F8C boundary. --- @@ -605,16 +606,17 @@ F8B may begin when explicitly tasked. Its own verification gate governs advancem - F7 O1 Commitment Admission is complete and G7 is closed. - 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. -- No O2 execution, settlement, delivery or finalization behavior has been introduced. +- F8B O2 Exact-Output Execution / Execution Evidence is complete and G8B is closed. +- No O2 settlement, delivery or finalization behavior has been introduced. **Current Gate** -- G8A — CLOSED / PASS. No gate is currently open. +- G8B — CLOSED / PASS. No gate is currently open. **Next Blocker** -- F8B — O2 Exact-Output Execution / Execution Evidence has not been started. +- F8C — Authoritative Settlement / Direct Beneficiary Delivery has not been started. **Immediate Next Step** -- Begin F8B when explicitly tasked. +- Begin F8C when explicitly tasked. diff --git a/docs/prompts/session-12-f8b-documentation-correction.md b/docs/prompts/session-12-f8b-documentation-correction.md new file mode 100644 index 0000000..3917508 --- /dev/null +++ b/docs/prompts/session-12-f8b-documentation-correction.md @@ -0,0 +1,212 @@ +# Session 12 — F8B Documentation Correction + +## 1. Working Model + +Use the established repository ownership model. + +- `CLAUDE.md` owns permanent operating behavior. +- `.claude/rules/*` owns permanent Solidity/testing conventions. +- This prompt owns only this bounded F8B correction, its scope, prohibitions, required evidence, and completion boundary. + +This is **not a new implementation slice**. + +F8B production behavior has already undergone independent review and is substantively accepted. + +The only open issue before G8B closure is stale F8A-era documentation that became factually incorrect after F8B introduced the protected PoolManager execution. + +Maintain the existing Session 12 implementation record: + +```text +docs/prompts/session-12-log.md +``` + +Record this correction as a material follow-up prompt and document exactly what changed. + +--- + +# 2. Objective + +Correct stale comments, NatSpec, or test documentation that still state or imply that: + +```text +no swap executes at this slice +``` + +or equivalent wording. + +That statement was valid during F8A but is no longer valid after F8B. + +F8B now performs the protected exact-output PoolManager execution. + +However: + +- F8B does not settle the resulting input debt; +- F8B does not enforce `actualInput <= maxInput`; +- F8B does not deliver protected output to the Beneficiary; +- those responsibilities remain assigned to F8C. + +The documentation must describe that boundary accurately. + +--- + +# 3. Required Semantic Correction + +The corrected documentation must preserve this distinction: + +```text +F8B: + protected exact-output swap executes + authoritative PoolManager execution evidence is established + actual input debt therefore exists + +F8C: + derive authoritative actual input debt for settlement + enforce actualInput <= maxInput + settle that debt + deliver protected output directly to the authoritative Beneficiary +``` + +Therefore, `maxInput` remains semantically inert **for F8B authorization/execution classification and evidence**, not because no swap occurs, but because enforcement of the resulting actual input debt belongs to F8C. + +A suitable formulation is: + +> `maxInput` remains semantically inert through F8B. The protected exact-output swap now executes and produces authoritative PoolManager input debt, but enforcement of `actualInput <= maxInput` belongs to F8C together with authoritative settlement. F8B therefore does not consume or enforce `maxInput`. + +Use wording appropriate to each local comment or test description; do not mechanically copy this text if a shorter formulation is clearer. + +--- + +# 4. Known Locations to Inspect + +At minimum inspect the stale F8A-era wording in: + +```text +src/ExerciseRouter.sol +``` + +particularly the `exercise(...)` NatSpec / `maxInput` explanation. + +Also inspect: + +```text +test/fuzz/ExerciseAuthorizationFuzz.t.sol +``` + +particularly documentation around: + +```text +testFuzz_maxInput_changesNoAuthorizationOutcome +``` + +where F8A-era wording may still state that no swap executes. + +Search the changed F8A/F8B files for materially equivalent stale claims so the correction is complete. + +Do not alter text merely because it predates F8B; change only statements that became factually false or misleading because F8B now executes R3. + +--- + +# 5. Scope + +This correction is documentation-only. + +Permitted changes: + +```text +comments +NatSpec +test comments +test explanatory documentation +docs/prompts/session-12-log.md +``` + +No production behavior should change. + +No test behavior should change. + +No assertion should change. + +No contract interface should change. + +No storage should change. + +No test fixture behavior should change. + +--- + +# 6. Prohibitions + +Do not: + +```text +modify F8B execution semantics +modify O2/O3 classification +modify the causal lifecycle +modify EXECUTING or EXECUTED behavior +change ExerciseRouter execution flow +implement or partially implement F8C +enforce maxInput +settle PoolManager input debt +deliver output to the Beneficiary +modify Remaining +modify authoritative O +perform fulfillment or finalization +begin GI +begin F9 +update docs/project-status.md +``` + +This correction must not reopen an already accepted implementation decision. + +--- + +# 7. Verification + +After the documentation correction, run the repository-standard verification required by the existing permanent instructions. + +At minimum confirm: + +```text +forge fmt --check +forge build +forge test +FOUNDRY_PROFILE=ci forge test +``` + +If the permanent repository rules require additional commands, follow those rules. + +The expected result is behavioral equivalence with the already reviewed F8B implementation. + +--- + +# 8. Required Completion Report + +Return a concise report containing: + +1. files inspected; +2. files changed; +3. each stale statement corrected; +4. confirmation that changes were documentation-only; +5. commands run; +6. verification results; +7. confirmation that no F8C/F8D behavior was introduced; +8. confirmation that `docs/project-status.md` was not changed; +9. updated `docs/prompts/session-12-log.md` entry for this material follow-up. + +Do not propose new F8B architecture or redesign unless the documentation correction unexpectedly exposes an actual contradiction in production behavior. + +--- + +# 9. Completion Boundary + +This task ends when: + +```text +stale F8A-era documentation corrected ++ +Session 12 log updated ++ +verification remains green +``` + +**Stop at the F8B documentation-correction boundary.** diff --git a/docs/prompts/session-12-f8b-o2-exact-output-execution.md b/docs/prompts/session-12-f8b-o2-exact-output-execution.md new file mode 100644 index 0000000..838c795 --- /dev/null +++ b/docs/prompts/session-12-f8b-o2-exact-output-execution.md @@ -0,0 +1,1364 @@ +# Standby — Claude Code Session 12 + +## F8B — O2 Exact-Output Execution / Execution Evidence + +You are implementing the next bounded slice of **Standby**, an ETHGlobal 2026 Uniswap v4 project. + +The current implementation state is already validated through: + +```text +F0 v4 Infrastructure + COMPLETE / G0 CLOSED + +F1 Deterministic Economic Fixture + COMPLETE / G1 CLOSED + +F2 EligibilityRegistry + COMPLETE / G2 CLOSED + +F3 StandbyHook Trust + PES Configuration + COMPLETE / G3 CLOSED + +F4 Commitment Storage + Bounded References + COMPLETE / G4 CLOSED + +F5 Authoritative Derivation Kernel + COMPLETE / G5 CLOSED + +F6A Preliminary O3 Enforcement with O = 0 + COMPLETE / G6A CLOSED + +F7 O1 Commitment Admission + COMPLETE / G7 CLOSED + +F6B O3 Enforcement with Authentic O > 0 + COMPLETE / G6B CLOSED + +F8A O2 Authorization / Hook-Owned Causal Context + COMPLETE / G8A CLOSED +``` + +The only authorized implementation slice for this session is: + +> **F8B — O2 Exact-Output Execution / Execution Evidence** + +Do not implement F8C settlement/delivery or F8D fulfillment/finalization. + +--- + +# 1. Repository Operating Model + +Follow the permanent repository instructions already owned by: + +```text +CLAUDE.md +.claude/rules/* +``` + +Do not duplicate or reinterpret those rules in this session. + +This prompt owns only the F8B-specific: + +- objective; +- responsibility boundary; +- required semantics; +- prohibited responsibility leakage; +- implementation scope; +- verification obligations; +- completion boundary. + +Before modifying production code: + +1. inspect the current repository implementation; +2. inspect the actual pinned Uniswap v4 source compiled by this repository; +3. inspect the frozen implementation plan and relevant canonical artifacts; +4. inspect the completed F8A implementation and tests; +5. reconstruct the actual existing transient-context representation; +6. verify all assumed v4 API and delta semantics against the pinned dependency rather than copying historical examples. + +If the actual pinned interfaces materially contradict a requirement in this prompt, stop and record the contradiction rather than silently changing protocol semantics. + +--- + +# 2. Session Implementation Record + +Maintain the contemporaneous implementation chronology in: + +```text +docs/prompts/session-12-log.md +``` + +The log should capture material implementation decisions, discoveries, commands/tests run, failures, corrections, and the eventual Claude-side assessment of G8B. + +Do not turn the log into a duplicate of permanent repository documentation. + +--- + +# 3. F8B Objective + +Implement exactly this responsibility: + +> **Bind one existing Hook-owned AUTHORIZED exercise to exactly one qualifying protected PoolManager swap, classify only that exact swap as O2, and transition the Hook-owned causal context to EXECUTED only after authoritative PoolManager callback evidence proves that the matching swap actually produced exactly the authorized protected output quantity `q`.** + +F8B therefore owns exactly two substantive responsibilities: + +1. **O2 Execution Classification** + - determine whether a proposed PoolManager swap is the exact execution authorized by the active F8A context; + +2. **Authoritative Execution Evidence** + - determine whether that exact swap actually executed for the full authorized protected output `q`. + +F8B does not own settlement, delivery, fulfillment, Remaining reduction, or finalization. + +--- + +# 4. Existing Authority Boundaries + +Preserve the established authority model. + +## 4.1 PoolManager + +The real immutable Uniswap v4 PoolManager is authoritative for: + +```text +actual AMM execution +actual swap result +actual PoolManager-produced BalanceDelta +actual post-swap v4 state +``` + +Only authenticated callbacks from the immutable PoolManager may create Standby execution evidence. + +## 4.2 StandbyHook + +The Hook is authoritative for: + +```text +Standby O2/O3 interpretation +matching an execution against Hook-owned AUTHORIZED context +Hook-owned transaction-scoped causal state +AUTHORIZED -> EXECUTED interpretation +``` + +## 4.3 ExerciseRouter + +The configured ExerciseRouter is a transaction coordinator. + +It does not own: + +```text +execution truth +commitment truth +Beneficiary truth +S +S′ +O +Remaining +fulfillment +a reusable execution ledger +``` + +Router intent, router return values, arbitrary calldata, and `hookData` are not proof that execution occurred. + +Preserve the rule: + +> **PoolManager owns execution truth; StandbyHook owns Standby causal interpretation; ExerciseRouter owns neither.** + +--- + +# 5. Existing F8A Context + +F8A already creates one transaction-scoped Hook-owned AUTHORIZED context after all authorization predicates pass. + +The context already binds the minimum causal facts required by later O2 stages, including the authoritative equivalent of: + +```text +service / pool identity +commitmentId +authenticated ExerciseRouter +authenticated originating exerciser +authoritative Beneficiary +exact q +causal state +``` + +F8A deliberately does not snapshot economic state such as: + +```text +S +S′ +O +Remaining +Validity +Exercisability +eligibility +``` + +Do not introduce duplicate snapshots in F8B. + +Existing F8A implementation-level states include: + +```text +EMPTY +AUTHORIZING +AUTHORIZED +``` + +`AUTHORIZING` is an implementation-only in-flight guard rather than a new economic lifecycle state. + +F8B may analogously add the implementation-only state: + +```text +EXECUTING +``` + +as specified below. + +--- + +# 6. F8B Causal State Model + +The canonical economic causal progression remains: + +```text +EMPTY + -> AUTHORIZED + -> EXECUTED + -> later consumed / EMPTY +``` + +For implementation-level callback isolation, F8B should use: + +```text +AUTHORIZED + -> EXECUTING + -> EXECUTED +``` + +`EXECUTING` means only: + +> the unique `beforeSwap` corresponding to this authorization has been accepted as the exact O2 swap, and its corresponding authoritative `afterSwap` is now expected. + +`EXECUTING` must not contain new economic truth. + +Do not persist this lifecycle in ordinary contract storage. + +Use the existing transaction-scoped context mechanism consistently. + +Do not introduce a persistent execution ledger. + +--- + +# 7. O2 Versus O3 Classification + +Before F8B, swaps use the existing ordinary O3 path. + +Preserve that behavior when no O2 operation is active. + +The required classification is: + +```text +context == EMPTY + -> swap follows existing ordinary O3 classification/enforcement +``` + +Once F8A has created an AUTHORIZED context, the system is inside an O2 causal exclusion zone: + +```text +context == AUTHORIZED + + exact authorized O2 swap + -> classify O2 + +context == AUTHORIZED + + any non-matching swap + -> REVERT +``` + +Do not silently route a mismatching swap through O3 while an authorization is unresolved. + +This restriction preserves causal continuity between the state used during F8A authorization and the swap execution that authorization intended. + +Likewise, an unresolved: + +```text +EXECUTING +``` + +or: + +```text +EXECUTED +``` + +context must not permit an unrelated swap to proceed as ordinary O3. + +ExerciseRouter identity alone must never classify a swap as O2. + +An AUTHORIZED context alone must never classify a swap as O2. + +`hookData` alone must never classify a swap as O2. + +A protected-direction swap alone must never classify a swap as O2. + +A q-like amount alone must never classify a swap as O2. + +Classification is conjunctive. + +--- + +# 8. Exact O2 `beforeSwap` Matching + +The actual proposed PoolManager swap must match the complete authorized O2 shape. + +At minimum verify the authoritative equivalent of all of the following. + +## 8.1 Callback authority + +The Hook callback itself must come from the immutable configured PoolManager. + +Reuse the existing trusted callback boundary rather than creating an alternate trust model. + +## 8.2 PoolManager operation sender + +The v4 callback `sender` for the canonical O2 swap must be the exact configured ExerciseRouter/O2 coordinator. + +This is a required conjunct. + +It is not by itself sufficient to establish O2. + +## 8.3 Exact service / PoolId + +The callback PoolKey must resolve to the exact configured service PoolId bound to the active authorization. + +No pool substitution is permitted. + +## 8.4 Protected direction + +Require: + +```text +params.zeroForOne == configured protected direction +``` + +Do not hard-code canonical zero-for-one into production logic. + +The canonical fixture is zero-for-one, but generalized one-for-zero behavior must remain correct. + +## 8.5 Exact-output mode + +Under the pinned v4 `SwapParams` semantics, verify the repository's actual sign convention. + +For the currently pinned v4 semantics this is expected to be: + +```text +amountSpecified > 0 + => exact output + +amountSpecified < 0 + => exact input +``` + +F8B must accept only exact-output O2. + +Do not infer this from memory; confirm against the installed dependency. + +## 8.6 Exact requested `q` + +The requested output quantity must exactly equal the quantity bound by F8A. + +Conceptually: + +```text +params.amountSpecified == q +``` + +subject to the actual pinned integer types and safe conversions. + +Reject: + +```text +q - 1 +q + 1 +0 +exact-input representation +overflowing / invalid conversion +``` + +## 8.7 Exact qualification boundary + +The O2 swap must use the authoritative qualification price boundary already defined by the configured service and consumed by the F5 prospective protected-exercise derivation. + +Verify: + +```text +params.sqrtPriceLimitX96 +``` + +matches that exact canonical execution boundary. + +Do not derive an alternate F8B price-limit policy. + +Do not duplicate F5 economics. + +--- + +# 9. `beforeSwap` Consequence + +A matching `beforeSwap` proves only: + +> this is the exact authorized O2 execution attempt. + +It does not prove that execution actually happened. + +Therefore a matching `beforeSwap` may transition: + +```text +AUTHORIZED + -> EXECUTING +``` + +It must not transition directly to: + +```text +EXECUTED +``` + +Router intent is not execution proof. + +Requested output is not actual output. + +A successful `beforeSwap` callback is not actual AMM execution evidence. + +--- + +# 10. Authoritative `afterSwap` Evidence + +Only the authoritative PoolManager `afterSwap` path may establish `EXECUTED`. + +The Hook must require the execution context to be: + +```text +EXECUTING +``` + +before accepting execution evidence. + +Revalidate the callback facts necessary to prove this `afterSwap` belongs to the exact accepted O2 swap, including the authoritative equivalent of: + +```text +immutable PoolManager callback authority +callback sender +PoolKey / PoolId +protected direction +exact-output mode +requested q +qualification price limit +``` + +Do not assume that the existence of `EXECUTING` alone permits arbitrary callback data to satisfy the execution. + +--- + +# 11. `BalanceDelta` Semantics and Actual Protected Output + +Verify the pinned v4 `BalanceDelta` semantics directly from the installed source. + +The expected current v4 interpretation is: + +```text +positive currency delta: + currency owed to the swap caller + +negative currency delta: + currency owed by the swap caller to PoolManager +``` + +For a protected zero-for-one exercise: + +```text +input = currency0 +output = currency1 + +actual protected output = delta.amount1() +``` + +The required successful execution evidence is: + +```text +delta.amount1() == +q +``` + +For a protected one-for-zero exercise: + +```text +input = currency1 +output = currency0 + +actual protected output = delta.amount0() +``` + +The required successful execution evidence is: + +```text +delta.amount0() == +q +``` + +Use the actual pinned types and safe conversions. + +Do not use absolute values. + +Do not accept a negative output-side delta. + +Do not accept partial output. + +Do not infer actual output from: + +```text +params.amountSpecified +router return values +router calldata +hookData +estimated execution +predicted S′ +``` + +The distinction is required: + +```text +beforeSwap: +requested exact output == q + +afterSwap: +actual protected output == q +``` + +Both must hold. + +--- + +# 12. Partial Exact-Output Execution + +Treat the following as failure: + +```text +requested exact output = q +actual PoolManager protected output < q +``` + +This matters because the existence of an exact-output request does not independently prove the full requested quantity was actually produced. + +A partial output must never produce: + +```text +EXECUTED +``` + +The containing swap/exercise should revert. + +--- + +# 13. `EXECUTING -> EXECUTED` + +The only valid transition to execution evidence is: + +```text +EXECUTING + + authoritative matching PoolManager afterSwap + + actual protected output == q + -> EXECUTED +``` + +`EXECUTED` means exactly: + +> **The unique PoolManager swap causally bound to the active authorization actually executed the exact authorized protected output quantity `q`.** + +It does not mean: + +```text +actual input debt has been paid +maxInput has been satisfied +input settlement is complete +Beneficiary has received q +output credit has been discharged +commitment has been fulfilled +Remaining has been reduced +O has been reduced +commitment has been finalized +``` + +--- + +# 14. Minimum Causal Binding + +Apply Semantic Minimality. + +The agreed F8B model does not require a new execution nonce, execution hash, router-owned success flag, or duplicated swap ledger merely to bind `beforeSwap` to `afterSwap`. + +The intended minimum binding is: + +```text +existing authoritative F8A context ++ +EXECUTING implementation guard ++ +immutable PoolManager callback ordering/authentication ++ +exact sender/PoolKey/SwapParams validation ++ +authoritative afterSwap BalanceDelta +``` + +Do not add an execution nonce/hash unless the actual pinned implementation mechanics demonstrate a concrete causal ambiguity that cannot otherwise be resolved. + +If such a contradiction is discovered, stop and document it before introducing new protocol state. + +--- + +# 15. Nested / Multiple / Reentrant Paths + +The MVP O2 model remains: + +```text +one commitment exercise +-> one qualifying protected exact-output swap +-> later one settlement/delivery path +-> later one finalization +``` + +Reject causal ambiguity. + +At minimum ensure the implementation cannot successfully perform: + +```text +EMPTY -> EXECUTING + +EMPTY -> EXECUTED + +AUTHORIZED -> second AUTHORIZED + +AUTHORIZED -> EXECUTED from beforeSwap alone + +AUTHORIZED -> unrelated ordinary O3 swap + +EXECUTING -> second beforeSwap + +EXECUTING -> nested unrelated swap + +EXECUTING -> second authorization + +EXECUTING -> EXECUTED without matching actual q + +EXECUTED -> second execution + +EXECUTED -> overwritten authorization + +EXECUTED -> unrelated second swap while the context remains unresolved +``` + +Do not introduce a broad generic reentrancy framework if the narrow causal-state machine already rejects the relevant transitions. + +--- + +# 16. Failed Swap / Caught-Revert Boundary + +The production ExerciseRouter must treat failure of the protected O2 PoolManager swap as failure of the containing exercise transaction. + +It must not implement: + +```text +authorize +-> try PoolManager swap +-> catch failed swap +-> retry another O2 swap +``` + +or: + +```text +authorize +-> failed O2 swap +-> catch +-> continue with unrelated protocol actions +``` + +A failed protected PoolManager execution must propagate. + +This matters because an `EXECUTING` transient-state write performed inside the reverted PoolManager callback path may roll back to the earlier AUTHORIZED state. + +The canonical production behavior therefore requires the failed PoolManager call to unwind the containing exercise, so the earlier F8A authorization also disappears. + +Do not introduce a new pre-PoolManager `beginExecution()` transition solely to support retry/catch behavior that the canonical ExerciseRouter does not permit. + +G8B must contain adversarial evidence that production behavior cannot convert a failed execution into a second execution attempt against the same authorization. + +--- + +# 17. Economic Atomicity + +The eventual complete O2 operation is economically atomic: + +```text +authorization ++ execution ++ settlement ++ Beneficiary delivery ++ finalization + +must ultimately be complete-or-zero +``` + +F8B does not independently finalize those economics. + +However, its transaction-scoped evidence must remain compatible with later atomic rollback. + +If F8C or F8D later fails, the containing transaction must be capable of reverting the earlier PoolManager execution and F8B causal evidence. + +Do not introduce an irreversible persistent F8B side effect that could survive an incomplete O2 transaction. + +Persistent commitment state must remain unchanged during F8B. + +--- + +# 18. Actual Input Debt / `maxInput` + +The actual PoolManager `BalanceDelta` may expose the input-side debt at the same point that F8B observes protected-output execution. + +Visibility does not assign responsibility. + +F8B must not implement: + +```text +actualInput <= maxInput +``` + +F8B must not settle input debt. + +F8B must not assign payment responsibility. + +F8B should not add actual input debt to Hook causal context merely because it is available during `afterSwap`, unless the pinned mechanics demonstrate that F8C cannot authoritatively recover the necessary settlement truth later in the same transaction. + +The current normative ownership is: + +```text +F8B: + prove actual protected output == q + +F8C: + derive authoritative actual input debt + enforce actualInput <= maxInput + settle that debt +``` + +`maxInput` remains semantically inert in F8B. + +--- + +# 19. F8C Responsibilities That Must Remain Absent + +Do not implement any F8C behavior. + +F8C later owns: + +```text +authoritative actual input debt +actualInput <= maxInput +exerciser payment +PoolManager input settlement +settlement-delta closure +PoolManager.take(...) +direct protected-output delivery to authoritative Beneficiary +Beneficiary balance consequence +output-side accounting closure +``` + +Do not send protected output to: + +```text +ExerciseRouter +StandbyHook +arbitrary caller-provided recipient +``` + +Do not claim delivery. + +Any mechanical PoolManager delta closure needed only by F8B tests must remain test-only and must not be presented as protocol settlement semantics. + +--- + +# 20. F8D Responsibilities That Must Remain Absent + +Do not implement: + +```text +Remaining reduction +authoritative O reduction +fulfillment +commitment completion +economic finalization +consumption of the complete O2 causal proof +``` + +F8B owns only execution evidence. + +Persistent commitment Remaining must remain unchanged. + +Aggregate Obligation must remain authoritative under the existing derivation and must not be reduced by F8B. + +--- + +# 21. Test-Only PoolManager Delta Closure + +A real PoolManager swap produces currency accounting that must eventually be closed for the surrounding unlock transaction to complete. + +Production settlement/delivery belongs to F8C. + +Therefore F8B integration/differential tests may use narrowly scoped **test-only mechanical delta closure** where required to allow the real PoolManager transition to commit and its post-state to be examined. + +Such test plumbing must not establish or claim: + +```text +maxInput semantics +economic payer semantics +Beneficiary delivery semantics +fulfillment +Remaining reduction +O reduction +production F8C behavior +``` + +Its purpose is only: + +> allow an otherwise valid real-PoolManager execution to complete sufficiently for F8B execution evidence and F5 predicted-versus-actual state equivalence to be tested. + +Keep this boundary explicit in test names/comments/helpers. + +Do not move equivalent settlement code into production as part of F8B. + +--- + +# 22. Predicted Versus Actual Pool State + +Reuse the existing F5 authoritative prospective-execution derivation. + +For successful allowed executions, G8B must include differential evidence that: + +```text +F5 predicted prospective PoolManager state +``` + +matches: + +```text +actual PoolManager post-swap state +``` + +and that predicted prospective Supporting Capacity: + +```text +S′ +``` + +matches authoritative Supporting Capacity derived from actual PoolManager post-state. + +Do not reimplement an F8B-specific swap simulator. + +Do not derive S or S′ using shortcuts such as token amount subtraction. + +Preserve the corrected F5 traversal semantics. + +--- + +# 23. Ordinary O3 Regression + +The introduction of O2 classification must not weaken the existing F6B ordinary-swap enforcement. + +Required behavior: + +```text +no O2 context +-> ordinary swaps continue through existing O3 path +``` + +An ordinary protected swap must not bypass: + +```text +prospective S′ +current O +S′ >= O +``` + +merely because F8B exists. + +Conversely, an exact authorized O2 swap must not accidentally fall through into the ordinary O3 rule, because O2 authorization used the prospective post-fulfillment condition: + +```text +S′ >= O - q +``` + +rather than ordinary O3: + +```text +S′ >= O +``` + +Preserve the distinction. + +--- + +# 24. Required Adversarial Cases + +G8B evidence must cover, as applicable to the actual implementation, all of the following threat families. + +## 24.1 Positive classification + +Prove an authentic: + +```text +AUTHORIZED context ++ +configured ExerciseRouter ++ +correct PoolId ++ +protected direction ++ +exact-output q ++ +correct qualification limit +``` + +is classified as the unique O2 swap. + +## 24.2 No authorization + +A swap with no active AUTHORIZED context cannot create O2 execution evidence. + +Ordinary swaps remain O3. + +## 24.3 Router identity alone + +Configured ExerciseRouter identity without a valid AUTHORIZED context is insufficient. + +## 24.4 Forged hookData + +Forged or imitative `hookData` cannot manufacture O2 classification or EXECUTED evidence. + +## 24.5 Wrong pool + +A swap against any other PoolId rejects while O2 is unresolved. + +## 24.6 Wrong direction + +Opposite direction cannot satisfy the authorization. + +## 24.7 Exact-input substitution + +An exact-input swap cannot satisfy an exact-output authorization. + +## 24.8 Quantity substitution + +Reject at least: + +```text +q - 1 +q + 1 +``` + +and other fuzzed mismatches. + +## 24.9 Qualification-boundary substitution + +A different `sqrtPriceLimitX96` cannot satisfy O2 even if other fields match. + +## 24.10 BeforeSwap-only proof + +Demonstrate that a matching `beforeSwap` does not itself produce EXECUTED. + +## 24.11 Partial actual output + +A requested exact-output `q` whose actual PoolManager protected output is less than `q` cannot produce EXECUTED. + +## 24.12 Malformed afterSwap / wrong evidence + +Wrong output side, wrong sign, wrong quantity, or unexpected callback ordering cannot produce EXECUTED. + +Use an appropriate harness only where direct malformed-callback testing cannot be obtained through the real PoolManager path. + +Do not weaken production callback authentication for testing. + +## 24.13 Second swap + +A second matching or unrelated swap against the same unresolved authorization rejects. + +## 24.14 Nested swap + +A nested PoolManager interaction cannot substitute for the expected execution. + +## 24.15 Second authorization + +Another authorization cannot overwrite or coexist with the unresolved execution context. + +## 24.16 Second execution after EXECUTED + +Execution evidence cannot be reused for another swap. + +## 24.17 Failed execution / catch-retry + +Prove the production ExerciseRouter does not catch a failed protected swap and retry or continue against the restored AUTHORIZED context. + +## 24.18 Cross-transaction replay + +No authorization/execution evidence can be reused in a later transaction. + +## 24.19 Commitment/service/actor/Beneficiary substitution + +Existing F8A bindings must remain intact; F8B must not create an execution path that can substitute another commitment, service, exerciser, or Beneficiary. + +## 24.20 Persistent-state non-interference + +After F8B execution evidence: + +```text +Remaining unchanged +authoritative O unchanged +``` + +No fulfillment is recorded. + +--- + +# 25. G8B — O2 Exact-Output Execution / Execution-Evidence Gate + +F8B is complete only when the implementation and evidence establish all of the following. + +### G8B-1 — Authorization prerequisite + +No swap obtains O2 semantics without an existing Hook-owned AUTHORIZED context. + +### G8B-2 — Router non-authority + +ExerciseRouter identity alone is insufficient to classify O2 or establish execution. + +### G8B-3 — Exact operation identity + +O2 requires exact agreement on: + +```text +configured ExerciseRouter callback sender +service / PoolId +protected direction +exact-output mode +exact requested q +qualification sqrtPriceLimitX96 +``` + +### G8B-4 — O2/O3 exclusion + +With EMPTY context, ordinary swaps retain the existing O3 path. + +With unresolved O2 causal state, only the exact authorized O2 path may proceed; mismatching swaps reject rather than silently becoming O3. + +### G8B-5 — BeforeSwap is not execution + +A matching `beforeSwap` establishes only the in-flight execution attempt and cannot produce EXECUTED. + +### G8B-6 — Minimal in-flight guard + +`EXECUTING` or an equivalent minimal transaction-scoped mechanism binds the accepted O2 `beforeSwap` to the expected authoritative `afterSwap` without introducing duplicate economic truth. + +### G8B-7 — PoolManager execution authority + +Only the immutable PoolManager callback path can advance execution evidence. + +Router assertions, hookData, arbitrary calldata, or external success flags cannot. + +### G8B-8 — Actual protected output + +`afterSwap` uses authoritative pinned-v4 `BalanceDelta` semantics to derive actual protected output in the configured output currency. + +### G8B-9 — Exact actual `q` + +Only: + +```text +actual protected output == +q +``` + +can establish EXECUTED. + +Partial, excessive, wrong-sign, or wrong-currency-side evidence rejects. + +### G8B-10 — Correct causal transition + +Only: + +```text +EXECUTING +-> matching authoritative afterSwap +-> actual protected output == q +-> EXECUTED +``` + +is permitted. + +No: + +```text +EMPTY -> EXECUTED +AUTHORIZED -> EXECUTED from intent/beforeSwap +second EXECUTED transition +execution overwrite +``` + +is permitted. + +### G8B-11 — Exactly one swap + +One authorization permits exactly the single canonical protected swap path. + +Nested/multiple/second execution paths reject. + +### G8B-12 — Failed execution atomicity + +A failed O2 execution propagates through the production ExerciseRouter and cannot be caught and converted into a retry or continuing O2 transaction. + +No false or reusable execution evidence survives failure. + +### G8B-13 — Predicted/actual equivalence + +For successful real-PoolManager execution: + +```text +F5 predicted post-state +== +actual PoolManager post-state +``` + +and: + +```text +predicted S′ +== +authoritative S from actual post-state +``` + +within the existing authoritative derivation model. + +Test-only mechanical delta closure may be used where needed without implementing F8C production semantics. + +### G8B-14 — Commitment state unchanged + +F8B does not change: + +```text +Remaining +persistent commitment terms +authoritative O by fulfillment +``` + +### G8B-15 — No Beneficiary-delivery assumption + +EXECUTED does not mean the Beneficiary received q. + +No production protected-output delivery is implemented in F8B. + +### G8B-16 — No input-settlement assumption + +EXECUTED does not mean actual input debt is settled. + +No `maxInput` enforcement is implemented. + +### G8B-17 — No fulfillment/finalization + +F8B does not implement F8D. + +### G8B-18 — O3 regression safety + +Existing F6B O3 backing enforcement remains intact and cannot be bypassed through the new O2 classifier. + +### G8B-19 — Prior gates preserved + +Previously closed F4/F5/F6A/F6B/F7/F8A behavior remains green. + +### G8B-20 — Verification breadth + +Appropriate: + +```text +unit +fuzz +transition/adversarial +real-PoolManager integration +predicted-vs-actual differential +``` + +evidence passes. + +Do not prematurely implement the later full GI stateful invariant campaign. + +--- + +# 26. Verification Technique Boundary + +The canonical proof obligations above are mandatory. + +The exact test file organization is downstream implementation discretion unless already constrained by repository conventions. + +Use the narrowest effective combination of: + +```text +unit tests +fuzz tests +integration tests +mutation-style negative cases +callback/order adversarial tests +differential tests +targeted harnesses where justified +``` + +Do not create a broad new invariant handler merely because this slice has adversarial requirements. + +The full stateful invariant campaign remains: + +```text +GI — Full Stateful Invariant Gate +``` + +after complete F8 and before G9. + +--- + +# 27. Responsibility-Leakage Gate + +Before declaring F8B complete, explicitly inspect the production diff and answer: + +1. Did F8B introduce any second authoritative source for commitment identity? +2. Did F8B duplicate S/S′/O derivation? +3. Did the ExerciseRouter acquire economic truth? +4. Did hookData acquire authority it did not previously have? +5. Did F8B introduce a persistent execution ledger? +6. Did F8B bind snapshots that later logic could derive authoritatively? +7. Did F8B implement input settlement? +8. Did F8B implement `actualInput <= maxInput`? +9. Did F8B implement Beneficiary delivery? +10. Did F8B reduce Remaining? +11. Did F8B reduce O as fulfillment? +12. Did F8B claim fulfillment or finalization? +13. Did test-only delta closure leak into production semantics? +14. Did O2 classification weaken ordinary O3 enforcement? + +Any affirmative answer requires justification against this prompt or correction before G8B can pass. + +--- + +# 28. Single Normative Ownership Review + +At completion, confirm: + +```text +commitment truth -> existing Hook commitment storage +economic derivations -> existing F5 derivation kernel +ordinary O3 enforcement -> existing F6A/F6B path +exercise authorization -> F8A Hook logic +authorized causal identity -> F8A Hook context +actual AMM execution -> PoolManager +O2 interpretation -> F8B Hook logic +actual-output execution evidence -> F8B Hook interpretation of PoolManager delta +input debt settlement -> future F8C +maxInput enforcement -> future F8C +Beneficiary delivery -> future F8C +fulfillment / Remaining change -> future F8D +``` + +Do not blur these owners. + +--- + +# 29. Semantic Minimality Review + +The preferred F8B causal addition is only: + +```text +EXECUTING +``` + +or its exact equivalent if the current implementation representation makes another minimal encoding clearer. + +Do not add: + +```text +execution nonce +execution hash +actualInput snapshot +S snapshot +S′ snapshot +O snapshot +Remaining snapshot +delivery flag +settlement flag +fulfillment flag +persistent execution status +``` + +without a demonstrated requirement. + +If implementation mechanics prove one additional transaction-scoped binding is genuinely necessary, document why the existing context plus callback facts are insufficient before adding it. + +--- + +# 30. Economic Atomicity Review + +Confirm that: + +```text +F8B execution evidence is transaction-scoped +``` + +and that later F8C/F8D failure can still cause the entire exercise to revert. + +No incomplete O2 transaction may leave irreversible F8B economic state. + +--- + +# 31. Required Final Claude Report + +When implementation and testing are complete, report: + +1. production files changed; +2. test/support files changed; +3. exact F8B causal-state implementation; +4. exact `beforeSwap` O2 matching logic; +5. exact `afterSwap` actual-output derivation; +6. how protected zero-for-one and one-for-zero output are interpreted; +7. how partial-output failure is proven; +8. how nested/second/replay paths are prevented; +9. how failed swap propagation/caught-revert risk is handled; +10. whether any extra causal field beyond `EXECUTING` was required and why; +11. how real-PoolManager integration testing closes deltas without introducing production F8C behavior; +12. predicted-versus-actual F5 differential evidence; +13. confirmation Remaining/O remain unchanged; +14. confirmation no settlement/delivery/finalization behavior was introduced; +15. exact test commands run and their results; +16. prior suites/regression evidence; +17. Claude's own G8B item-by-item assessment; +18. any unresolved concern that should prevent G8B closure. + +Do not declare the project ready for F8C merely because your own tests pass. + +ChatGPT will independently review the implementation and independently determine whether G8B closes. + +--- + +# 32. Completion Boundary + +This Claude session ends at: + +```text +F8B implemented ++ +G8B evidence produced ++ +session-12-log.md updated +``` + +Stop at the F8B completion boundary. diff --git a/docs/prompts/session-12-log.md b/docs/prompts/session-12-log.md new file mode 100644 index 0000000..ce194f7 --- /dev/null +++ b/docs/prompts/session-12-log.md @@ -0,0 +1,410 @@ +# Session 12 — Material Prompt Log + +Initiating prompt: `docs/prompts/session-12-f8b-o2-exact-output-execution.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 — Correct stale F8A-era documentation about `maxInput` and the protected swap + +**Instruction.** `docs/prompts/session-12-f8b-documentation-correction.md`, issued after independent +review of the completed F8B implementation. The review accepted F8B production behavior +substantively and identified one remaining issue before G8B closure: documentation written during +F8A that still states or implies **"no swap executes at this slice"**, which stopped being true when +F8B introduced the protected exact-output PoolManager execution. The prompt requires the corrected +wording to preserve the real boundary — the swap now executes and authoritative input debt therefore +exists, but deriving that debt, enforcing `actualInput <= maxInput`, and settling it remain F8C's — +so `maxInput` is inert through F8B because enforcement belongs to F8C, not because no swap occurs. +The correction is documentation-only: no production behavior, test behavior, assertion, interface, +storage, or fixture behavior may change, no F8C/F8D responsibility may be introduced, and +`docs/project-status.md` must not be updated. + +**Consequence.** Four stale statements were corrected and one helper description was brought up to +date. No Solidity outside comments and NatSpec changed; `forge test` and +`FOUNDRY_PROFILE=ci forge test` both report the same 466 passing tests as before the correction. +The corrections are recorded under **Correction 1** at the end of this log. + +### Prompt 2 — Synchronize `docs/project-status.md` with the reviewed F8B result + +**Instruction.** Update **status only** in `docs/project-status.md` to reflect the independently +reviewed F8B result: F8B — O2 Exact-Output Execution / Execution Evidence **COMPLETE**, G8B **PASS**, +and F8C — Authoritative Settlement / Direct Beneficiary Delivery as the **next authorized +implementation slice / current blocker**. No implementation details, design commentary, test +summaries, gate reasoning, retrospective observations, reachability qualifications, 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, 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, correction record, 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 F8B and F8C; the §7 line that still said O2 execution was +unimplemented; the §10 current blocker and its 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`, `docs/project-status.md`, the Session 12 prompt, + `docs/implementation-plan.md` §14 (F8A), §15 (F8B) and §16 (F8C, for the boundary), and + `docs/uniswap-v4-realization.md` §13 (RR-O2-6 … RR-O2-10), §14, §15 and §16. +2. Inspected the pinned Uniswap v4 dependency directly rather than relying on memory: + - `lib/v4-hooks-public/lib/v4-core/src/types/PoolOperation.sol` — `SwapParams.amountSpecified` + is documented as "the desired input amount if negative (exactIn), or the desired output + amount if positive (exactOut)". The prompt's expected sign convention is confirmed against + the installed source. + - `lib/v4-hooks-public/lib/v4-core/src/types/BalanceDelta.sol` — `amount0()`/`amount1()` are the + packed `int128` halves. + - `lib/v4-hooks-public/lib/v4-core/src/PoolManager.sol` `swap` — the `BalanceDelta` handed to + `afterSwap` is the swap delta accounted to `msg.sender` of `PoolManager.swap`, so a positive + currency amount is owed *to* the swap caller and a negative amount is owed *by* it. + - `lib/v4-hooks-public/lib/v4-core/src/libraries/Pool.sol` `swap` — for an exact-output swap the + specified side of the delta is `amountSpecified - amountSpecifiedRemaining`, i.e. the amount + actually produced, which is strictly less than `q` when the price limit is reached first. + Partial exact-output execution is therefore an ordinary success for Uniswap and must be + rejected by Standby. + - `lib/v4-hooks-public/lib/v4-core/src/libraries/Hooks.sol` — `beforeSwap`/`afterSwap` are + handed `msg.sender` of `PoolManager.swap` as `sender`, and the `params` given to `afterSwap` + are the original caller-proposed `SwapParams`. + - `lib/v4-hooks-public/src/base/BaseHook.sol` — `_beforeSwap` and `_afterSwap` are non-view + `virtual`, so the causal-state writes this slice needs require no base change. +3. Inspected the completed F8A implementation and its evidence: `src/StandbyHook.sol`, + `src/ExerciseRouter.sol`, `test/shared/BaseExerciseAuthorizationTest.t.sol`, + `test/shared/BaseUnbackedExerciseAuthorizationTest.t.sol`, `test/harness/StandbyHookHarness.sol`, + and the four F8A suites. + +--- + +## Implementation Decisions + +### The production ExerciseRouter acquires R3, and F8B therefore has no completable production path + +`implementation-plan.md` §14.0 decomposes the router into R1–R5, and the F8 slices map onto it +one-for-one: R1+R2 is F8A, **R3 — exactly one protected exact-output PoolManager execution — is +F8B**, R4 is F8C, R5 is F8D. Session-12 prompt §16 and G8B-12 require adversarial evidence about +what the *production* ExerciseRouter does when the protected swap fails, which is only meaningful +if the production router is the contract issuing that swap. R3 is therefore implemented here. + +The direct consequence is that no production O2 transaction can complete at F8B. A real +PoolManager swap opens currency deltas, and `PoolManager.unlock` reverts with `CurrencyNotSettled` +unless they are closed; closing them is F8C's input settlement and Beneficiary delivery, which this +slice must not implement. The prompt anticipates exactly this in §21 and permits narrowly scoped +**test-only mechanical delta closure** so that an otherwise valid real-PoolManager execution can +commit and be examined. + +That closure is `test/harness/ExerciseDeltaClosureRouter.sol`: a subclass of the production +`ExerciseRouter` that overrides `unlockCallback` alone, runs the complete production execution path +through it, and then closes whatever deltas the swap left from its own pre-funded balance. It +assigns no payer, enforces no `maxInput`, and delivers nothing to the Beneficiary — the protected +output is parked on the closure contract precisely so that no test can mistake it for delivery. + +### Consequence for the F8A suites + +Because a successful `exercise(...)` now continues into execution, the F8A fixture is activated +with the delta-closure router and the successful F8A paths end at `EXECUTED` rather than +`AUTHORIZED`. Every F8A authorization predicate is still proven by the same tests against the same +production `authorizeExercise`, and every F8A rejection test is unchanged, because each rejection +still occurs inside `authorizeExercise` before any unlock. Two F8A tests state facts that F8B +legitimately supersedes and are recorded under **F8A test adaptations** below. + +### One reconstruction of the admitted protected execution, consumed by everything + +F8A already reconstructed the canonical protected execution inline inside +`_prospectiveExerciseCapacity` in order to decide backing. F8B needs the same object in three more +places: the `beforeSwap` classifier, the `afterSwap` revalidation, and the proposal the router hands +the PoolManager. Restating it would have created up to four descriptions of what a Standby exercise +executes, which is exactly the duplication the authoritative-derivation rule forbids, so the +reconstruction was extracted to `_canonicalProtectedExecution(q)` and every consumer resolves +through it. The authorization decision and the execution requirement are therefore the same object +rather than two things that have to be kept in agreement. + +The router obtains it through the new external view `authorizedProtectedExecution()`, which +describes the execution the *currently AUTHORIZED* context admits and reverts otherwise. That read +grants nothing: the Hook revalidates the operation on the authoritative callback path, so a router +that ignores what it read is refused exactly as one that never read it. The alternative — a router +composing direction, exact-output mode and `P_Q` itself — would have been a second production +statement of the qualification boundary, which §8.7 of the session prompt prohibits. + +### Classification is one equality, not four policies + +`_requireAuthorizedProtectedExecution` compares the proposed `SwapParams` against that single +reconstruction. Protected direction, exact-output mode, exact `q`, and the qualification price limit +are therefore not four independent checks that could drift apart; they are the one operation the +service defines. Callback sender identity is a separate, explicitly necessary-but-insufficient +conjunct, and the pool is established by `_requireConfiguredServicePool` on every path. + +The pool is deliberately *not* re-compared against `context.serviceId`. The service is one-shot and +immutable and the authorization stamped the context with `_serviceId()`, so their equality is a +theorem rather than an assumption, and adding the comparison would have been duplicated checking. + +### No extra causal field beyond `EXECUTING` + +The minimum binding the prompt describes was sufficient. `beforeSwap` and `afterSwap` for one swap +are ordered and authenticated by the immutable PoolManager inside a single call frame nothing else +can enter, the Hook already holds every fact both callbacks must agree with, and `EXECUTING` records +that a proposal was accepted. No execution nonce, hash, router success flag, or swap ledger was +required, and no snapshot of any economic quantity was added. + +### F8A test adaptations + +Two F8A tests asserted facts that F8B changes, and both were rewritten rather than weakened: + +- `test_successfulAuthorization_leavesEveryAuthoritativeFactUntouched` asserted that a successful + request changes *no* authoritative state, including pool state. A request now performs the + protected execution, so the pool legitimately moves. It became + `test_completedRequest_fulfilsNothingAndDeliversNothing`, which asserts the fulfillment half that + is still true and is the half G8A actually required: the commitment record, Remaining Entitlement, + the derived obligation, the bounded reference, the identity counter, and the Beneficiary's and the + Hook's protected-output balances all unchanged. +- `test_authorizedContext_doesNotAdvanceBeyondAuthorized` asserted that an ordinary swap proceeds + normally while an authorization is live, because `AUTHORIZED -> EXECUTED` did not exist. F8B + requires the opposite: while an O2 causal context is unresolved the ordinary path is closed. The + test was removed and replaced by the stronger + `ProtectedExecutionTest.test_ordinarySwap_whileAnO2ContextIsUnresolved_isRejected`. + +One F8A fuzz property gained a branch. `ExerciseBackingFuzz` explores harness-written obligations the +pool never backed, and inside that region an authorization can be admitted for a quantity the pool +cannot deliver before `P_Q`. Those runs now execute and are refused by the execution evidence rather +than by the backing comparison, which the test asserts explicitly — and reaching that refusal is +itself evidence that the authorization was admitted. It also became the fuzzed real-PoolManager +evidence for partial-output rejection. + +### Direction generality + +The canonical fixture is protected `zeroForOne`, so a hard-coded direction would have passed every +other suite. `ProtectedExecutionDirectionTest` activates a real second service whose protected +direction is `oneForZero` and proves both direction-dependent mechanics mirror: the admitted +operation is the `oneForZero` exact-output swap bounded by a `P_Q` above the current price, and the +protected output is read from currency0. No liquidity or swap is needed, because neither mechanic +consults them. + +--- + +## 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 (F8A), §15 (F8B), §16 (F8C boundary) +- `docs/uniswap-v4-realization.md` §13 (RR-O2-6 … RR-O2-10), §14, §15, §16 +- `docs/prompts/session-12-f8b-o2-exact-output-execution.md`, `docs/prompts/session-11-log.md` +- `src/StandbyHook.sol`, `src/ExerciseRouter.sol`, `src/demo/ActorAwareTestRouter.sol` +- `script/helpers/StandbyFixtureConfig.sol` +- `test/shared/BaseActorAwareStandbyTest.t.sol`, `BaseCommitmentAdmissionTest.t.sol`, + `BaseAuthenticBackingTest.t.sol`, `BaseExerciseAuthorizationTest.t.sol`, + `BaseUnbackedExerciseAuthorizationTest.t.sol`, `BaseDerivationTest.t.sol`, + `ReferenceCalculations.sol` +- `test/harness/StandbyHookHarness.sol`, `StandbyDerivationHarness.sol` +- the four F8A suites and `test/integration/ProspectiveStateEquivalence.t.sol` + +Pinned dependency: + +- `lib/v4-hooks-public/lib/v4-core/src/types/PoolOperation.sol` (exact-output sign convention) +- `lib/v4-hooks-public/lib/v4-core/src/types/BalanceDelta.sol` +- `lib/v4-hooks-public/lib/v4-core/src/PoolManager.sol` (`swap`, `_swap`, `unlock`) +- `lib/v4-hooks-public/lib/v4-core/src/libraries/Pool.sol` (`swap` delta construction) +- `lib/v4-hooks-public/lib/v4-core/src/libraries/Hooks.sol` (`beforeSwap`, `afterSwap`) +- `lib/v4-hooks-public/src/base/BaseHook.sol`, `lib/v4-hooks-public/lib/v4-periphery/src/base/ImmutableState.sol` +- `lib/v4-hooks-public/lib/v4-core/src/libraries/SafeCast.sol` + +### Files Changed + +Production: + +- `src/StandbyHook.sol` — added `EXECUTING`/`EXECUTED` to the transaction-scoped causal lifecycle; + added five F8B rejection conditions; split `beforeSwap` into the ordinary-O3 rule and the O2 + execution classifier; gave `afterSwap` the O2 execution-evidence branch; added + `_beginProtectedExecution`, `_recordProtectedExecution`, `_requireAuthorizedProtectedExecution`, + `_actualProtectedOutput`, `_canonicalProtectedExecution`, and the `authorizedProtectedExecution()` + read surface; re-expressed `_prospectiveExerciseCapacity` through the one reconstruction. +- `src/ExerciseRouter.sol` — added R3: the router now unlocks the PoolManager and performs exactly + one protected execution, asked of the Hook rather than composed here, with no failure branch. + +Test support: + +- `test/harness/ExerciseDeltaClosureRouter.sol` (new) — production router plus test-only mechanical + delta closure. +- `test/harness/AdversarialExerciseRouter.sol` (new) — hostile configured O2 coordinator. +- `test/harness/StandbyHookHarness.sol` — three F8B pass-throughs: write a causal context, run the + classifier, run the evidence mechanic. +- `test/shared/BaseExerciseAuthorizationTest.t.sol` — configures the delta-closure router, funds its + closure balance, adds the `_expectedContextRouter` seam and the `_assertExercisedContext` + assertion. +- `test/shared/BaseAdversarialExerciseTest.t.sol` (new) — same fixture with a hostile router. + +Tests: + +- `test/integration/ProtectedExecution.t.sol` (new) +- `test/unit/ProtectedExecutionEvidence.t.sol` (new) +- `test/fuzz/ProtectedExecutionFuzz.t.sol` (new) +- `test/periphery/ProtectedExecutionPerimeter.t.sol` (new) +- `test/integration/ExerciseAuthorization.t.sol`, `test/unit/ExerciseAuthorization.t.sol`, + `test/fuzz/ExerciseAuthorizationFuzz.t.sol`, `test/fuzz/ExerciseBackingFuzz.t.sol`, + `test/periphery/ExerciseAuthorizationPerimeter.t.sol` — F8A adaptations described above. + +Documentation: + +- `docs/prompts/session-12-log.md` (new, this file). + +### Requirements Implemented + +F8B — O2 execution classification and authoritative execution evidence: `implementation-plan.md` +§15.1–§15.5; `uniswap-v4-realization.md` RR-O2-7, RR-O2-8, RR-O2-9, RR-O2-4, RR-O2-5, §16.3; and +§§3–20 of the session prompt. Router R3 of `implementation-plan.md` §14.0. + +### Tests Added or Changed + +See **Files Changed**. Coverage by gate item is recorded under **Gate Evidence** below. + +### Commands Run + +``` +forge fmt --check +forge lint +forge build +forge test --match-path 'test/**/Exercise*' +forge test --match-path test/integration/ProtectedExecution.t.sol +forge test --match-path test/unit/ProtectedExecutionEvidence.t.sol +forge test --match-path test/fuzz/ProtectedExecutionFuzz.t.sol +forge test --match-path test/periphery/ProtectedExecutionPerimeter.t.sol +forge test +FOUNDRY_PROFILE=ci forge test +``` + +### Results + +`forge fmt --check` clean. `forge lint` reports nothing. `forge build` succeeds. +`forge test`: 466 tests passed, 0 failed, 0 skipped, across 47 suites. +`FOUNDRY_PROFILE=ci forge test`: 466 tests passed, 0 failed, 0 skipped (fuzz runs 10,000). + +### Gate Evidence + +Implemented and verified: G8B-1 through G8B-11, G8B-13 through G8B-19. +Verified with a documented reachability caveat: G8B-12. +Not evaluated: none of G8B; the stateful invariant campaign (GI) remains out of scope. + +### Known Limitations / Blockers + +1. No production O2 transaction can complete at F8B. The protected execution opens PoolManager + deltas that only F8C may close, so every production exercise reverts at the unlock boundary. + Positive execution evidence therefore comes through the test-only delta-closure router. +2. Genuinely nested reentry (`EXECUTING -> second beforeSwap`) is unreachable through the real + PoolManager, because the accepted swap's `beforeSwap` and `afterSwap` occur inside one call frame + nothing else can enter. It is verified at unit level against the production predicate instead. +3. Partial actual output is likewise unreachable from a backed state, and is verified through the + real PoolManager from a harness-written unbacked remainder. + +### Scope Check + +Within F8B. No settlement, `maxInput` enforcement, Beneficiary delivery, Remaining reduction, +obligation reduction, fulfillment, or finalization was implemented, and no persistent state was +added. `docs/project-status.md` was deliberately not modified. + +### Proposed Gate Assessment + +PASS (proposed only). Every G8B condition has passing implementation and verification evidence, with +the reachability caveats recorded above. G8B closure is ChatGPT's to determine. + +### Recommended Next Step + +F8C — authoritative input settlement, `maxInput` enforcement against the actual PoolManager debt, +and direct Beneficiary delivery of exactly `q`. Not authorized by this session. + +### Prompt Audit + +All material follow-up instructions were recorded. **0** material follow-up prompts were issued in +this session; the initiating prompt was executed as written. + +--- + +## Correction 1 — Stale F8A-era documentation + +Issued by `docs/prompts/session-12-f8b-documentation-correction.md` after independent review of the +F8B implementation. Documentation-only. + +### What was stale + +F8A's ExerciseRouter executed no swap, so the `maxInput` field could be described as inert *because +there was no input debt to bound*. F8B implements R3: the protected exact-output swap now executes, +and it produces authoritative PoolManager input debt. The reason `maxInput` is still inert therefore +changed, and any wording resting on the old reason became false. + +The corrected boundary is: + +```text +F8B: the protected exact-output swap executes + authoritative PoolManager execution evidence is established + actual input debt therefore exists + +F8C: derive the authoritative actual input debt + enforce actualInput <= maxInput + settle that debt + deliver protected output directly to the authoritative Beneficiary +``` + +### Statements corrected + +1. `src/ExerciseRouter.sol` — `exercise(...)` NatSpec. Was: "…and no swap executes here, so there is + no debt to bound and nothing to enforce it against yet… it is the F8A boundary made structural". + Now: the debt exists because the protected swap executes here, and deriving it, requiring + `actualInput <= maxInput`, and settling it are one responsibility that belongs to F8C — so the + bound is not enforced here, and the parameter stays unnamed as the settlement boundary made + structural. +2. `test/fuzz/ExerciseAuthorizationFuzz.t.sol` — `testFuzz_maxInput_changesNoAuthorizationOutcome` + NatSpec. Was: "…and no swap executes at this slice". Now: the swap executes and the debt exists, + but enforcing the bound belongs to F8C together with settling it, so nothing consumes it yet. The + property's claim was widened from "no F8A outcome" to "no outcome", which is what the test + actually asserts now that a permissible request also executes. +3. `test/periphery/ExerciseAuthorizationPerimeter.t.sol` — `test_maxInputOfZero_authorizesIdentically` + NatSpec. Was: "…there is no debt at this slice". Now: the bound is measured against the input debt + the executed swap produces, and enforcing it belongs to F8C together with settling that debt. +4. `test/shared/BaseExerciseAuthorizationTest.t.sol` — `UNCONSTRAINED_MAX_INPUT`. Was: "F8A neither + reads nor forwards this field". Now: nothing implemented so far reads or forwards it, and + enforcement against the executed swap's input debt belongs to F8C. + +One further description was brought up to date rather than corrected for falsehood: +`_authorizeAs` in `test/shared/BaseExerciseAuthorizationTest.t.sol` described itself as requesting +authorization; it now states that the request is the whole production coordination — authorization +followed by the one protected execution that authorization admits — so a refused request never +reaches an execution and a passing one leaves execution evidence rather than a standing +authorization. + +### Statements deliberately left unchanged + +- `StandbyHook.authorizeExercise` NatSpec ("It executes no swap, moves no price, settles no + input…"). Its subject is the authorization transition itself, which genuinely executes no swap; + the swap is performed by the router's unlock afterwards. +- `StandbyHook` contract NatSpec ("Authorization is not exercise. It executes no swap…"), for the + same reason, and because the following paragraph already states what execution evidence is and is + not. +- `ExerciseRouter` contract NatSpec ("Because they are absent, no exercise completes at this + slice"). This is an F8B statement written this session and remains true: the unsettled deltas + unwind every production exercise. +- `test/integration/O3AuthenticBackingEnforcement.t.sol` ("Fulfillment does not exist yet"), which + remains true — F8B attributes no fulfillment. + +### Verification + +``` +forge fmt --check clean +forge lint no findings +forge build successful +forge test 466 passed, 0 failed, 0 skipped +FOUNDRY_PROFILE=ci forge test 466 passed, 0 failed, 0 skipped +``` + +Behaviorally identical to the reviewed F8B implementation: no production logic, test logic, +assertion, interface, storage layout, or fixture behavior changed. `docs/project-status.md` was not +modified. diff --git a/src/ExerciseRouter.sol b/src/ExerciseRouter.sol index b0a98ad..cba89ce 100644 --- a/src/ExerciseRouter.sol +++ b/src/ExerciseRouter.sol @@ -5,6 +5,12 @@ pragma solidity 0.8.26; IMPORTS //////////////////////////////////////////////////////////////*/ +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {IUnlockCallback} from "v4-core/interfaces/callback/IUnlockCallback.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {SwapParams} from "v4-core/types/PoolOperation.sol"; + import {StandbyHook} from "./StandbyHook.sol"; import {IActorAwarePeriphery} from "./interfaces/IActorAwarePeriphery.sol"; @@ -14,11 +20,19 @@ import {IActorAwarePeriphery} from "./interfaces/IActorAwarePeriphery.sol"; /// @title ExerciseRouter /// @notice The designated O2 coordinator of one Protected Execution Service. -/// @dev At implementation slice F8A this contract owns exactly two responsibilities, the first two of the -/// five the reference realization assigns it (`implementation-plan.md` §14.0): it accepts the external -/// exercise request, and it preserves the originating exerciser across the call into the Hook so that -/// the Hook can recover an authenticated economic actor. Protected execution, input settlement, -/// Beneficiary delivery, and finalization are later slices and are deliberately absent here. +/// @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. +/// +/// 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. /// /// 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 @@ -44,7 +58,7 @@ import {IActorAwarePeriphery} from "./interfaces/IActorAwarePeriphery.sol"; /// The originator is the direct caller of `exercise`. `tx.origin` is never consulted, and there is no /// 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 { +contract ExerciseRouter is IActorAwarePeriphery, IUnlockCallback { /*////////////////////////////////////////////////////////////// STATE VARIABLES //////////////////////////////////////////////////////////////*/ @@ -60,6 +74,11 @@ contract ExerciseRouter is IActorAwarePeriphery { /// Hook's own one-shot service activation decides that. StandbyHook public immutable i_hook; + /// @notice The PoolManager the coordinated protected execution is performed against. + /// @dev Resolved from the Hook rather than supplied separately, so this router cannot be bound to a + /// PoolManager the Hook does not answer callbacks from. + IPoolManager public immutable i_poolManager; + /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -71,6 +90,10 @@ contract ExerciseRouter is IActorAwarePeriphery { /// @param exerciser The originator of the request already in flight. error ExerciseRouter__ExerciseContextAlreadyActive(address exerciser); + /// @notice Thrown when the unlock callback is invoked by anything other than the PoolManager. + /// @param caller The unauthorized caller. + error ExerciseRouter__NotPoolManager(address caller); + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -79,6 +102,7 @@ contract ExerciseRouter is IActorAwarePeriphery { /// @param _hook The StandbyHook that owns the Protected Execution Service. constructor(StandbyHook _hook) { i_hook = _hook; + i_poolManager = _hook.poolManager(); } /*////////////////////////////////////////////////////////////// @@ -92,24 +116,38 @@ contract ExerciseRouter is IActorAwarePeriphery { /// 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 at this slice it is request-surface data and nothing else. - /// It is exercise-local cost protection, enforced against the authoritative PoolManager input debt - /// that an executed exact-output swap produces (RR-O2-10, RR-O2-12) — and no swap executes here, - /// so there is no debt to bound and nothing to enforce it against yet. Its parameter is therefore - /// 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 F8A boundary made structural, - /// and the settlement stage that can honour the bound is the stage that should name it. + /// 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. /// - /// A successful call leaves a Hook-owned AUTHORIZED causal context in place for the remainder of - /// the transaction. That context is authorization and nothing more: no swap has executed, no - /// input has been settled, no output has been delivered, no commitment has been fulfilled, and no - /// Remaining Entitlement has been reduced. The later O2 slices consume it. + /// 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. + /// + /// 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. + /// + /// 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. /// /// The originator context is cleared on the way out, so this router exposes no reusable identity - /// afterwards. The Hook's own authorization context is not cleared here, and must not be: whether - /// an authorization may be replaced, consumed, or repeated is a Standby economic question the Hook - /// owns, and a router that could clear it would be able to manufacture a second authorization. + /// afterwards. The Hook's own causal context is not cleared here, and must not be: whether it may + /// be replaced, consumed, or repeated is a Standby economic question the Hook owns, and a router + /// 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 { @@ -117,9 +155,31 @@ contract ExerciseRouter is IActorAwarePeriphery { i_hook.authorizeExercise(_commitmentId, _q); + i_poolManager.unlock(bytes("")); + _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. + /// + /// 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. + /// @return result The encoded balance delta of the performed execution. + function unlockCallback(bytes calldata) external virtual returns (bytes memory result) { + result = abi.encode(_executeAuthorizedExercise()); + } + /// @inheritdoc IActorAwarePeriphery function msgSender() external view returns (address actor) { actor = _exerciserContext(); @@ -131,6 +191,18 @@ contract ExerciseRouter is IActorAwarePeriphery { INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /// @dev Runs the authorized protected execution inside the PoolManager unlock. + /// + /// 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) { + if (msg.sender != address(i_poolManager)) revert ExerciseRouter__NotPoolManager(msg.sender); + + (PoolKey memory key, SwapParams memory params) = i_hook.authorizedProtectedExecution(); + + delta = i_poolManager.swap(key, params, bytes("")); + } + /// @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/src/StandbyHook.sol b/src/StandbyHook.sol index 3d14643..725a42f 100644 --- a/src/StandbyHook.sol +++ b/src/StandbyHook.sol @@ -39,20 +39,35 @@ import {StandbyMath} from "./libraries/StandbyMath.sol"; /// @title StandbyHook /// @notice The Standby Uniswap v4 Hook. -/// @dev At implementation slice F8A this contract owns the Hook-wide immutable trust basis, one one-shot +/// @dev At implementation slice F8B this contract owns the Hook-wide immutable trust basis, one one-shot /// Protected Execution Service configuration, the authoritative commitment record store with its /// bounded enforcement-reference index, the composition of the authoritative economic derivation /// kernel, the admission or rejection of ordinary O3 backing-affecting pool transitions, the O1 -/// admission that turns a proposed commitment into an authoritative binding one, and the O2 -/// authorization that binds one exercise attempt into a transaction-scoped causal context. +/// admission that turns a proposed commitment into an authoritative binding one, the O2 +/// authorization that binds one exercise attempt into a transaction-scoped causal context, and the +/// O2 classification and execution evidence that bind exactly one PoolManager swap to that context. /// /// Authorization is not exercise. It executes no swap, settles no input, delivers nothing to the -/// Beneficiary, fulfils nothing, and reduces no Remaining Entitlement, so no production path reduces -/// Remaining Entitlement and no fulfillment can occur at this slice. What authorization establishes +/// Beneficiary, fulfils nothing, and reduces no Remaining Entitlement. What authorization establishes /// is that exactly one commitment, one authenticated exerciser, one authoritative Beneficiary, one /// service, one ExerciseRouter, and one quantity have been bound together by the Hook before any /// protected execution could be attempted. /// +/// Execution evidence is not fulfillment either. `EXECUTED` says that the unique PoolManager swap +/// causally bound to the active authorization actually produced exactly the authorized protected +/// output `q` — and nothing beyond that. It does not say that the input debt has been paid, that the +/// exerciser's cost bound was honoured, that the Beneficiary received anything, that the commitment +/// was fulfilled, or that any Remaining Entitlement or Aggregate Capacity Obligation moved. No +/// production path at this slice reduces Remaining Entitlement, so no fulfillment can occur. +/// +/// What separates a Standby exercise from an ordinary swap is therefore not who asked for it. It is +/// the conjunction of an existing Hook-owned authorization, the bound ExerciseRouter as the +/// PoolManager operation sender, the configured service pool, the protected direction, exact-output +/// mode, the exact authorized quantity, the configured qualification boundary — and then, separately +/// and afterwards, authoritative PoolManager evidence that the swap actually produced `q`. Router +/// identity alone, `hookData` alone, an authorization alone, a protected-direction swap alone, and a +/// `q`-shaped amount alone each classify nothing. +/// /// O1 admission is where Aggregate Capacity Obligation first becomes positive. Enforcement was never /// told that: it obtains the obligation from the same derivation that reported zero while no /// commitment could exist, so a positive obligation changes what that derivation returns and nothing @@ -193,21 +208,33 @@ contract StandbyHook is BaseHook { /// @notice The lifecycle position of the transaction-scoped O2 causal context. /// @dev The frozen O2 causal lifecycle is `EMPTY -> AUTHORIZED -> EXECUTED -> consumed/EMPTY` - /// (`uniswap-v4-realization.md` §16). This slice owns exactly one arc of it, `EMPTY -> AUTHORIZED`, - /// so `EXECUTED` and consumption are deliberately not expressible here: they belong to the - /// execution and finalization slices, and declaring them early would let something claim a - /// transition no code can make. - /// - /// `AUTHORIZING` is not a causal lifecycle position. It is the in-flight marker of one - /// authorization attempt, written before the authorization consults anything outside this Hook and - /// replaced by `AUTHORIZED` only if every predicate succeeds. It exists so that an overlapping - /// authorization cannot arise during those external reads, and it can never be observed by a - /// successful call: a completed authorization reads `AUTHORIZED`, and a failed one reverts, which - /// discards the transient write with everything else. + /// (`uniswap-v4-realization.md` §16). This slice owns the arcs up to `EXECUTED`; consumption + /// belongs to finalization and is deliberately not expressible here, because declaring it early + /// would let something claim a transition no code can make. + /// + /// Two of these positions are implementation guards rather than causal lifecycle positions, and + /// neither carries economic truth of its own. + /// + /// `AUTHORIZING` is the in-flight marker of one authorization attempt, written before the + /// authorization consults anything outside this Hook and replaced by `AUTHORIZED` only if every + /// predicate succeeds. It exists so that an overlapping authorization cannot arise during those + /// external reads, and it can never be observed by a successful call: a completed authorization + /// reads `AUTHORIZED`, and a failed one reverts, which discards the transient write with + /// everything else. + /// + /// `EXECUTING` means exactly one thing: the unique `beforeSwap` corresponding to this + /// authorization has been accepted as the exact O2 swap, and its corresponding authoritative + /// `afterSwap` is now expected. It is what binds the accepted proposal to the evidence that + /// follows it, and it is the reason no execution nonce, execution hash, or swap ledger is needed: + /// the PoolManager itself orders and authenticates the two callbacks, and the Hook already holds + /// the facts both of them must agree with. A matching `beforeSwap` reaches `EXECUTING` and can + /// reach nothing further, because a proposal is not an execution. enum ExerciseAuthorizationState { EMPTY, AUTHORIZING, - AUTHORIZED + AUTHORIZED, + EXECUTING, + EXECUTED } /// @notice The complete transaction-scoped causal context of one authorized exercise attempt. @@ -654,6 +681,53 @@ contract StandbyHook is BaseHook { uint256 prospectiveCapacity, uint256 prospectiveObligation ); + /// @notice Thrown when a swap is proposed while the O2 causal context admits no protected execution. + /// @dev The causal exclusion zone, reported as one condition. Once an authorization exists the service + /// is inside an O2 operation, and the only swap that may proceed is the exact protected execution + /// that authorization admits — so a swap arriving against an unresolved `AUTHORIZING`, + /// `EXECUTING`, or `EXECUTED` context is refused rather than quietly re-classified as an ordinary + /// O3 transition it would then be enforced as under the wrong rule. + /// @param state The causal position the context was actually in. + error StandbyHook__ExerciseExecutionNotAuthorized(ExerciseAuthorizationState state); + + /// @notice Thrown when the PoolManager operation sender is not the ExerciseRouter the context bound. + /// @dev Distinct from `StandbyHook__NotExerciseRouter`, which reports that an *authorization request* + /// did not arrive through the configured ExerciseRouter. This one reports that the account that + /// asked the PoolManager to perform the swap is not the router the active authorization is bound + /// to. It is a required conjunct of O2 classification and never sufficient on its own. + /// @param sender The callback sender that proposed the swap. + error StandbyHook__NotAuthorizedExerciseExecutor(address sender); + + /// @notice Thrown when a proposed swap is not the exact protected execution the authorization admits. + /// @dev One rejection for one requirement: the proposed swap must be the canonical protected execution + /// reconstructed from the immutable service basis and the authorized quantity. Direction, + /// exact-output mode, the exact quantity, and the qualification boundary are that one shape, and a + /// swap that differs in any of them is not the authorized execution. The proposed shape is + /// reported, because the admitted one is derivable from the service and the causal context. + /// @param zeroForOne The proposed swap direction. + /// @param amountSpecified The proposed amount, negative for exact input and positive for exact output. + /// @param sqrtPriceLimitX96 The proposed square-root price limit. + error StandbyHook__NotTheAuthorizedProtectedExecution( + bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96 + ); + + /// @notice Thrown when execution evidence is offered while no protected execution is in flight. + /// @dev Execution evidence is only meaningful for a swap this Hook already accepted as the exact O2 + /// execution. A context that is not `EXECUTING` has no accepted swap awaiting evidence, so + /// `EMPTY -> EXECUTED`, `AUTHORIZED -> EXECUTED`, and a second `EXECUTED` transition are all + /// refused here rather than being distinguished into separate conditions they do not have. + /// @param state The causal position the context was actually in. + error StandbyHook__NoProtectedExecutionInFlight(ExerciseAuthorizationState state); + + /// @notice Thrown when the actual protected output of the executed swap is not exactly `q`. + /// @dev The authoritative execution evidence, and the one requirement that separates a swap that was + /// requested from a swap that happened. The output is taken from the PoolManager's own + /// `BalanceDelta` on the configured protected-output side, so a partial exact-output execution, an + /// output on the wrong currency side, and a wrong-signed delta all fail the same comparison. + /// @param actualProtectedOutput The protected-output amount the PoolManager actually produced. + /// @param authorizedQuantity The protected-output quantity the authorization admits. + error StandbyHook__ProtectedOutputNotExecuted(int256 actualProtectedOutput, uint256 authorizedQuantity); + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -967,6 +1041,33 @@ contract StandbyHook is BaseHook { context = _readExerciseAuthorization(); } + /// @notice Returns the one protected execution the active authorization admits. + /// @dev The single production expression of what a Standby exercise executes. The ExerciseRouter has to + /// propose a swap to the PoolManager, and the shape of that swap is not the router's to choose: + /// the pool, the protected direction, exact-output mode, the quantity, and the qualification + /// boundary `P_Q` are all facts this Hook already owns (RR-O2-7, RR-O2-8). Reconstructing them + /// here — from the same function the authorization backing derivation uses — is what makes the + /// authorized execution and the executable execution the same object rather than two descriptions + /// that have to be kept in agreement. + /// + /// Reading this grants nothing. It is a proposal, and the proposal is validated again on the + /// authoritative PoolManager callback path against the causal context, so a caller that ignores + /// what it read here is refused exactly as a caller that never read it. + /// + /// It reverts unless an authorization is currently AUTHORIZED, because outside that position there + /// is no admitted execution to describe. + /// @return key The configured service pool the execution is performed against. + /// @return params The exact protected exact-output swap the authorization admits. + function authorizedProtectedExecution() external view returns (PoolKey memory key, SwapParams memory params) { + ExerciseAuthorizationContext memory context = _readExerciseAuthorization(); + + if (context.state != ExerciseAuthorizationState.AUTHORIZED) { + revert StandbyHook__ExerciseExecutionNotAuthorized(context.state); + } + + (key, params) = (_service.poolKey, _canonicalProtectedExecution(context.q)); + } + /// @notice Returns the complete authoritative Protected Execution Service basis. /// @dev Before activation every field reads as its zero value and `configured` is false. /// @return service The persisted service basis. @@ -1127,12 +1228,62 @@ contract StandbyHook is BaseHook { INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @dev Decides whether a proposed ordinary swap may become an authoritative pool transition. + /// @dev Classifies a proposed swap and decides whether it may become an authoritative pool transition. /// - /// Every reachable swap here is an ordinary O3 transition. O2 classification requires an - /// authoritative exercise causal context, no production path can establish one yet, and router - /// identity alone never classifies a swap as O2 — a commitment-shaped payload from any router is - /// ordinary activity. + /// Classification is by causal context, never by who asked. With no O2 operation in progress every + /// swap is an ordinary O3 transition and is enforced as one, whoever routed it and whatever + /// `hookData` it carries. Once an authorization exists the service is inside an O2 causal + /// exclusion zone, and the only swap that may proceed is the exact protected execution that + /// authorization admits; anything else is refused rather than quietly re-classified. + /// + /// The exclusion is not tidiness. O2 was authorized against the prospective post-fulfillment + /// condition `S' >= O - q`, while an ordinary transition must satisfy `S' >= O`. Letting a + /// mismatching swap fall through to the ordinary rule while an authorization is unresolved would + /// break the causal continuity between the state the authorization was decided on and the + /// execution it authorized. + function _beforeSwap(address _sender, PoolKey calldata _key, SwapParams calldata _params, bytes calldata) + internal + virtual + override + returns (bytes4 selector, BeforeSwapDelta delta, uint24 lpFeeOverride) + { + _requireConfiguredServicePool(_key); + + if (_exerciseAuthorizationState() == ExerciseAuthorizationState.EMPTY) { + _requireAdmissibleOrdinarySwap(_sender, _params); + } else { + _beginProtectedExecution(_sender, _params); + } + + (selector, delta, lpFeeOverride) = (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + /// @dev Completes a swap callback: inert for an ordinary transition, authoritative evidence for O2. + /// + /// For an ordinary O3 transition this is completion plumbing and nothing else. The backing + /// decision was already made, from derived prospective state, before the transition became + /// authoritative, and nothing here is execution evidence, fulfillment proof, or settlement. + /// + /// For the one protected execution this Hook accepted, it is the only place execution evidence can + /// come from. The PoolManager has performed the swap by the time this runs and hands over its own + /// `BalanceDelta`, which is the sole authoritative statement of what the pool actually produced. + /// + /// It takes no delta, because the Hook declares no return-delta permission. + function _afterSwap( + address _sender, + PoolKey calldata _key, + SwapParams calldata _params, + BalanceDelta _delta, + bytes calldata + ) internal virtual override returns (bytes4 selector, int128 hookDelta) { + if (_exerciseAuthorizationState() != ExerciseAuthorizationState.EMPTY) { + _recordProtectedExecution(_sender, _key, _params, _delta); + } + + (selector, hookDelta) = (IHooks.afterSwap.selector, int128(0)); + } + + /// @dev Decides whether a proposed ordinary swap may become an authoritative pool transition. /// /// The decision sequence separates four questions that must never merge. Whether the callback is /// authentic: `onlyPoolManager` answers that before this runs. Whether the transition belongs to @@ -1155,15 +1306,7 @@ contract StandbyHook is BaseHook { /// change, so the same comparison covers both, and covering both is what makes the enforcement /// surface complete rather than merely correct on the expected path. This is not symmetry for its /// own sake: there is no second formula here, only one authoritative derivation used once. - function _beforeSwap(address _sender, PoolKey calldata _key, SwapParams calldata _params, bytes calldata) - internal - view - virtual - override - returns (bytes4 selector, BeforeSwapDelta delta, uint24 lpFeeOverride) - { - _requireConfiguredServicePool(_key); - + function _requireAdmissibleOrdinarySwap(address _sender, SwapParams calldata _params) internal view { if (_sender != i_trustedUniversalRouter) revert StandbyHook__UntrustedSwapPerimeter(_sender); address actor = _authenticatedActor(_sender); @@ -1173,27 +1316,136 @@ contract StandbyHook is BaseHook { (uint160 sqrtPriceX96, uint128 liquidity) = _prospectiveSwapState(_params); _requireProspectiveBacking(sqrtPriceX96, liquidity); + } - (selector, delta, lpFeeOverride) = (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + /// @dev Accepts a proposed swap as the one protected execution the active authorization admits. + /// + /// What it establishes is exactly this: the swap in front of the PoolManager right now is the + /// execution attempt this Hook authorized. What it deliberately does not establish is that any + /// execution happened. Router intent is not execution proof, a requested exact output is not an + /// actual output, and a successful `beforeSwap` is not AMM execution — so this reaches `EXECUTING` + /// and can reach nothing further. + /// + /// The state requirement is the whole causal restriction in one place. `AUTHORIZED` is the only + /// position that admits a swap: an `AUTHORIZING` context has not decided yet, an `EXECUTING` one + /// already has its swap in flight, and an `EXECUTED` one has already had it. A second swap, a + /// nested swap, and a swap after execution are therefore the same refusal rather than three + /// special cases. + function _beginProtectedExecution(address _sender, SwapParams calldata _params) internal { + ExerciseAuthorizationContext memory context = _readExerciseAuthorization(); + + if (context.state != ExerciseAuthorizationState.AUTHORIZED) { + revert StandbyHook__ExerciseExecutionNotAuthorized(context.state); + } + + _requireAuthorizedProtectedExecution(_sender, _params, context); + + _writeExerciseAuthorizationState(ExerciseAuthorizationState.EXECUTING); + } + + /// @dev Establishes that the accepted protected execution actually produced exactly `q`. + /// + /// This is the only transition to execution evidence, and it consumes exactly one authoritative + /// fact that no earlier stage had: the PoolManager's own `BalanceDelta` for the swap it has just + /// performed. Nothing else here is new. The context is the one the authorization wrote, and the + /// callback facts are revalidated against it rather than assumed from the presence of `EXECUTING`, + /// so evidence cannot be satisfied by an unrelated callback that merely arrives while a protected + /// execution is in flight. + /// + /// The comparison is exact and signed. Only the protected-output side of the delta counts, only a + /// positive amount is output owed to the swap caller, and only exactly `q` is the authorized + /// quantity — so a partial exact-output execution, an amount on the input side, and a wrong-signed + /// delta all fail it. A partial execution is a failure of the complete O2 rather than a smaller + /// exercise (RR-O2-9), and refusing it here reverts the containing transaction with it. + /// + /// What `EXECUTED` does not mean is as load-bearing as what it does. The input debt is unpaid, the + /// exerciser's cost bound is unexamined, the Beneficiary has received nothing, the commitment is + /// unfulfilled, and Remaining Entitlement and Aggregate Capacity Obligation are untouched. + function _recordProtectedExecution( + address _sender, + PoolKey calldata _key, + SwapParams calldata _params, + BalanceDelta _delta + ) internal { + ExerciseAuthorizationContext memory context = _readExerciseAuthorization(); + + if (context.state != ExerciseAuthorizationState.EXECUTING) { + revert StandbyHook__NoProtectedExecutionInFlight(context.state); + } + + _requireConfiguredServicePool(_key); + _requireAuthorizedProtectedExecution(_sender, _params, context); + + int256 actualProtectedOutput = _actualProtectedOutput(_delta); + + if (actualProtectedOutput != context.q.toInt256()) { + revert StandbyHook__ProtectedOutputNotExecuted(actualProtectedOutput, context.q); + } + + _writeExerciseAuthorizationState(ExerciseAuthorizationState.EXECUTED); } - /// @dev Completes an ordinary O3 swap callback and does nothing else. + /// @dev Requires a callback swap to be exactly the protected execution a causal context admits. + /// + /// Classification is conjunctive, and this is the conjunction. The account that asked the + /// PoolManager to perform the swap must be the ExerciseRouter the context bound — necessary, never + /// sufficient — and the proposed swap must equal the canonical protected execution reconstructed + /// from the immutable service basis and the authorized quantity. That single equality is where + /// protected direction, exact-output mode, the exact quantity `q`, and the configured + /// qualification boundary `P_Q` are all decided, because they are not four independent policies: + /// they are the one execution the service defines, and comparing against it is what makes an + /// alternative price limit, an exact-input substitution, an opposite direction, and a `q ± 1` + /// substitution the same rejection. + /// + /// The pool is not compared here. Every path into this function has already established that the + /// callback concerns the configured service pool, and a context's `serviceId` is that same pool by + /// construction: the service is one-shot and immutable, and the authorization stamped the context + /// with it. + function _requireAuthorizedProtectedExecution( + address _sender, + SwapParams calldata _params, + ExerciseAuthorizationContext memory _context + ) internal view { + if (_sender != _context.exerciseRouter) revert StandbyHook__NotAuthorizedExerciseExecutor(_sender); + + SwapParams memory authorized = _canonicalProtectedExecution(_context.q); + + if ( + _params.zeroForOne != authorized.zeroForOne || _params.amountSpecified != authorized.amountSpecified + || _params.sqrtPriceLimitX96 != authorized.sqrtPriceLimitX96 + ) { + revert StandbyHook__NotTheAuthorizedProtectedExecution( + _params.zeroForOne, _params.amountSpecified, _params.sqrtPriceLimitX96 + ); + } + } + + /// @dev Reads the protected-output side of an executed swap's authoritative PoolManager delta. /// - /// `afterSwap` is enabled because the exercise slice will need it, so every permitted ordinary - /// swap invokes it and it cannot be left failing closed. What it must not do is acquire that later - /// responsibility early: it is not execution evidence, not fulfillment proof, not settlement, and - /// it mutates nothing. The backing decision was already made, from derived prospective state, - /// before the transition became authoritative. + /// Which side that is follows from the configured protected direction and from nothing else: + /// protected `zeroForOne` takes currency0 in and produces currency1, protected `oneForZero` is its + /// mirror. The value is returned signed and unmodified, because its sign is authoritative — a + /// positive amount is currency the PoolManager owes the swap caller, which is what "produced" means + /// here, and a negative amount is currency the caller owes. Taking an absolute value would turn a + /// debt into a delivery. + function _actualProtectedOutput(BalanceDelta _delta) internal view returns (int256 actualProtectedOutput) { + actualProtectedOutput = _service.protectedZeroForOne ? _delta.amount1() : _delta.amount0(); + } + + /// @dev Reconstructs the one protected execution the service admits for a quantity. /// - /// It takes no delta, because the Hook declares no return-delta permission. - function _afterSwap(address, PoolKey calldata, SwapParams calldata, BalanceDelta, bytes calldata) - internal - pure - virtual - override - returns (bytes4 selector, int128 hookDelta) - { - (selector, hookDelta) = (IHooks.afterSwap.selector, int128(0)); + /// The protected execution is not a free choice and is never taken from a request. It is the one + /// the frozen realization defines: the service's own protected direction, exact-output for exactly + /// `_q`, bounded by the configured qualification boundary `P_Q` (RR-O2-7, RR-O2-8). Every consumer + /// of that shape — the authorization backing derivation, the execution classifier, the evidence + /// revalidation, and the router's own proposal — resolves through this one reconstruction, so the + /// quantity that was authorized and the quantity that is executable are the same question. + function _canonicalProtectedExecution(uint256 _q) internal view returns (SwapParams memory params) { + params = SwapParams({ + zeroForOne: _service.protectedZeroForOne, + amountSpecified: _q.toInt256(), + sqrtPriceLimitX96: TickMath.getSqrtPriceAtTick(_service.tickQ) + }); } /// @dev Decides whether a proposed liquidity addition may become an authoritative pool transition. @@ -1391,23 +1643,15 @@ contract StandbyHook is BaseHook { /// @dev Derives the Supporting Capacity the canonical protected exercise of exactly `q` would leave. /// - /// The protected execution is not a free choice and is not taken from the request. It is the one - /// the frozen realization defines: the service's own protected direction, exact-output for exactly - /// `q`, bounded by the configured qualification boundary `P_Q` (RR-O2-7, RR-O2-8). Reconstructing - /// it from the immutable service basis is what makes the authorized quantity and the executable - /// quantity the same question. + /// The execution being measured is the one the service admits, reconstructed by the single + /// production reconstruction rather than restated here — so the swap this authorization is decided + /// against is byte-for-byte the swap the execution classifier will later require. /// /// No arithmetic happens here. The prospective state comes from the same F5 derivation ordinary /// transitions use, and the same capacity kernel measures it, so an authorization decision and an /// enforcement decision can never disagree about what a state is worth. function _prospectiveExerciseCapacity(uint256 _q) internal view returns (uint256 capacity) { - SwapParams memory params = SwapParams({ - zeroForOne: _service.protectedZeroForOne, - amountSpecified: _q.toInt256(), - sqrtPriceLimitX96: TickMath.getSqrtPriceAtTick(_service.tickQ) - }); - - (uint160 sqrtPriceX96, uint128 liquidity) = _prospectiveSwapState(params); + (uint160 sqrtPriceX96, uint128 liquidity) = _prospectiveSwapState(_canonicalProtectedExecution(_q)); capacity = _prospectiveSupportingCapacity(sqrtPriceX96, liquidity); } diff --git a/test/fuzz/ExerciseAuthorizationFuzz.t.sol b/test/fuzz/ExerciseAuthorizationFuzz.t.sol index f2fc2f0..c183ff5 100644 --- a/test/fuzz/ExerciseAuthorizationFuzz.t.sol +++ b/test/fuzz/ExerciseAuthorizationFuzz.t.sol @@ -47,7 +47,7 @@ contract ExerciseAuthorizationFuzzTest is BaseExerciseAuthorizationTest { if (q > 0 && q <= entitlement) { _authorizeAs(commitmentExerciseAuthority, commitmentId, q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, q, "a permissible extent must be authorized" ); @@ -91,7 +91,7 @@ contract ExerciseAuthorizationFuzzTest is BaseExerciseAuthorizationTest { if (exerciser == commitmentExerciseAuthority) { _authorizeAs(exerciser, commitmentId, entitlement); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, exerciser, beneficiary, entitlement, "the exercise authority must be authorized" ); } else { @@ -110,13 +110,14 @@ contract ExerciseAuthorizationFuzzTest is BaseExerciseAuthorizationTest { REQUEST-SURFACE INERTNESS //////////////////////////////////////////////////////////////*/ - /// @notice The exerciser's `maxInput` changes no F8A outcome anywhere in the `uint256` domain. - /// @dev The field is on the frozen F8A request surface and has no F8A semantics: it is exercise-local + /// @notice The exerciser's `maxInput` changes no outcome anywhere in the `uint256` domain. + /// @dev The field is on the frozen request surface and still has no semantics here: it is exercise-local /// cost protection, enforced against the authoritative PoolManager input debt an executed - /// exact-output swap produces, and no swap executes at this slice. This property is what "no - /// semantics" means operationally — across every value the domain admits, a refusal is the same - /// refusal, an authorization is the same authorization, the causal bindings are identical, and the - /// derived economics are untouched. + /// exact-output swap produces. The swap now executes and that debt exists, but requiring + /// `actualInput <= maxInput` belongs to F8C together with settling it, so nothing consumes the + /// bound yet. This property is what "no semantics" means operationally — across every value the + /// domain admits, a refusal is the same refusal, an authorized exercise is the same exercise, the + /// causal bindings are identical, and the derived economics are untouched. /// /// Both directions are covered in one run. The refusal comes first, because a refusal leaves the /// single authorization slot free, so the same run can then show that the same bound authorizes a @@ -138,7 +139,7 @@ contract ExerciseAuthorizationFuzzTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, q, _maxInput); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, q, "no maxInput may alter the causal bindings" ); diff --git a/test/fuzz/ExerciseBackingFuzz.t.sol b/test/fuzz/ExerciseBackingFuzz.t.sol index d1748f1..6ca708a 100644 --- a/test/fuzz/ExerciseBackingFuzz.t.sol +++ b/test/fuzz/ExerciseBackingFuzz.t.sol @@ -5,6 +5,8 @@ pragma solidity 0.8.26; IMPORTS //////////////////////////////////////////////////////////////*/ +import {IHooks} from "v4-core/interfaces/IHooks.sol"; + import {StandbyHook} from "../../src/StandbyHook.sol"; import {BaseUnbackedExerciseAuthorizationTest} from "../shared/BaseUnbackedExerciseAuthorizationTest.t.sol"; @@ -55,6 +57,15 @@ contract ExerciseBackingFuzzTest is BaseUnbackedExerciseAuthorizationTest { /// an open window, an eligible Beneficiary, and `0 < q <= Remaining` — so the only decision left is /// the backing comparison. A refused run must name the exact independently predicted pair, and must /// leave the obligation, the remainder, and the causal context untouched. + /// + /// An authorized run whose quantity exceeds what the pool can deliver before `P_Q` is refused on + /// the other side of the operation, by the execution evidence rather than by the backing + /// comparison, and that distinction is asserted rather than smoothed over. Such a run exists only + /// because the harness wrote an obligation the pool never backed: inside a reachable state + /// `S >= O >= q` holds, so the exact-output execution always produces exactly `q`. Here it produces + /// everything the domain has and no more, which is a partial individual exercise, and RR-O2-9 + /// makes that a failure of the complete O2 rather than a smaller exercise. That the run reaches the + /// execution rejection at all is itself evidence that its authorization was admitted. function testFuzz_authorization_isAdmittedExactlyWhenTheCompleteExerciseStaysBacked( uint256 _remainingSeed, uint256 _qSeed, @@ -79,11 +90,23 @@ contract ExerciseBackingFuzzTest is BaseUnbackedExerciseAuthorizationTest { uint256 prospectiveObligation = obligation - q; if (expectedProspective >= prospectiveObligation) { - _authorizeAs(commitmentExerciseAuthority, commitmentId, q); - - _assertAuthorizationContext( - commitmentId, commitmentExerciseAuthority, beneficiary, q, "a backed exercise must be authorized" - ); + if (q > capacity) { + _expectHookRejection( + IHooks.afterSwap.selector, + abi.encodeWithSelector( + StandbyHook.StandbyHook__ProtectedOutputNotExecuted.selector, int256(capacity), q + ) + ); + _authorizeAs(commitmentExerciseAuthority, commitmentId, q); + + _assertNoAuthorizationContext("an undeliverable exercise must leave no causal context"); + } else { + _authorizeAs(commitmentExerciseAuthority, commitmentId, q); + + _assertExercisedContext( + commitmentId, commitmentExerciseAuthority, beneficiary, q, "a backed exercise must be authorized" + ); + } } else { vm.expectRevert( abi.encodeWithSelector( diff --git a/test/fuzz/ProtectedExecutionFuzz.t.sol b/test/fuzz/ProtectedExecutionFuzz.t.sol new file mode 100644 index 0000000..1ae27aa --- /dev/null +++ b/test/fuzz/ProtectedExecutionFuzz.t.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {TickMath} from "v4-core/libraries/TickMath.sol"; +import {toBalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {SwapParams} from "v4-core/types/PoolOperation.sol"; + +import {StandbyFixtureConfig} from "../../script/helpers/StandbyFixtureConfig.sol"; + +import {StandbyHook} from "../../src/StandbyHook.sol"; + +import {BaseExerciseAuthorizationTest} from "../shared/BaseExerciseAuthorizationTest.t.sol"; +import {BaseUnbackedExerciseAuthorizationTest} from "../shared/BaseUnbackedExerciseAuthorizationTest.t.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Fuzz evidence that O2 classification and execution evidence are exact (G8B-3, G8B-9). +/// @dev The two F8B decisions are equality decisions, and an equality decision is exactly the kind that +/// fails at one raw unit, one tick, or one flipped flag. Both are therefore explored across their +/// whole input shape rather than at chosen points: an operation is admitted if and only if it is the +/// one the service defines for the authorized quantity, and a delta is execution evidence if and only +/// if its protected-output side is exactly that quantity. +/// +/// Both properties are stated independently of the production branch structure. The expected outcome +/// of every run is computed from the frozen service semantics — the canonical protected direction, the +/// exact-output convention of the pinned `SwapParams`, the authorized quantity, and the configured +/// qualification boundary — and never from the derivation under test. +/// +/// The causal positions these mechanics guard exist only inside a PoolManager call frame, so the +/// harness supplies the starting position and nothing else. Every predicate exercised is production. +contract ProtectedExecutionFuzzTest 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 input side of a plausible protected execution delta. + int128 internal constant PLAUSIBLE_INPUT = -20_050_000_000; + + /// @dev The span of limit ticks explored. Wide enough to include both service boundaries, both + /// liquidity boundaries, and the current price, so a near miss is as likely as a far one. + int24 internal constant LIMIT_TICK_SPAN = 600; + + /*////////////////////////////////////////////////////////////// + O2 OPERATION IDENTITY + //////////////////////////////////////////////////////////////*/ + + /// @notice Exactly one operation is the authorized protected execution. + /// @dev Direction, mode and quantity, and qualification boundary are fuzzed together, so a run misses + /// on one conjunct, several, or none. The admitted case is the single point where all three agree + /// with the service's own definition; everything else is refused, and the refusal names the + /// operation that was proposed. + function testFuzz_onlyTheServiceDefinedOperation_isClassifiedO2( + bool _zeroForOne, + int256 _amountSeed, + int24 _limitTickSeed + ) public { + int256 amountSpecified = bound(_amountSeed, -int256(SEEDED_Q) - 2, int256(SEEDED_Q) + 2); + int24 limitTick = int24(bound(int256(_limitTickSeed), -LIMIT_TICK_SPAN, LIMIT_TICK_SPAN)); + + SwapParams memory proposed = SwapParams({ + zeroForOne: _zeroForOne, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: TickMath.getSqrtPriceAtTick(limitTick) + }); + + bool admitted = _zeroForOne == StandbyFixtureConfig.PROTECTED_DIRECTION_ZERO_FOR_ONE + && amountSpecified == int256(SEEDED_Q) && limitTick == StandbyFixtureConfig.TICK_Q; + + _seedContext(StandbyHook.ExerciseAuthorizationState.AUTHORIZED); + + if (!admitted) { + vm.expectRevert( + abi.encodeWithSelector( + StandbyHook.StandbyHook__NotTheAuthorizedProtectedExecution.selector, + proposed.zeroForOne, + proposed.amountSpecified, + proposed.sqrtPriceLimitX96 + ) + ); + } + + serviceHarness.beginProtectedExecution(exerciseRouter, proposed); + + assertEq( + uint256(_authorizationContext().state), + uint256( + admitted + ? StandbyHook.ExerciseAuthorizationState.EXECUTING + : StandbyHook.ExerciseAuthorizationState.AUTHORIZED + ), + "only the service-defined operation may advance the causal context" + ); + } + + /// @notice No account other than the bound ExerciseRouter can propose the authorized execution. + /// @dev The operation is the admitted one on every run, so the sender is the only thing deciding. + function testFuzz_onlyTheBoundRouter_mayProposeTheExecution(address _sender) public { + _seedContext(StandbyHook.ExerciseAuthorizationState.AUTHORIZED); + + if (_sender != exerciseRouter) { + vm.expectRevert( + abi.encodeWithSelector(StandbyHook.StandbyHook__NotAuthorizedExerciseExecutor.selector, _sender) + ); + } + + serviceHarness.beginProtectedExecution(_sender, _protectedExactOutputSwapParams(SEEDED_Q)); + } + + /*////////////////////////////////////////////////////////////// + EXECUTION EVIDENCE + //////////////////////////////////////////////////////////////*/ + + /// @notice Exactly the authorized quantity on the protected-output side, and nothing else, is evidence. + /// @dev The output side is fuzzed across both signs and both sides of the authorized quantity, and the + /// input side is fuzzed independently so that no run can pass by reading the wrong half of the + /// delta. Partial output, excess output, a debt-signed amount, and nothing at all are all refused + /// by the same comparison. + function testFuzz_onlyExactlyQ_isExecutionEvidence(int128 _outputSeed, int128 _inputSeed) public { + int128 output = int128(bound(int256(_outputSeed), -int256(SEEDED_Q) - 3, int256(SEEDED_Q) + 3)); + int128 input = int128(bound(int256(_inputSeed), -int256(SEEDED_Q) * 2, int256(SEEDED_Q) * 2)); + + _seedContext(StandbyHook.ExerciseAuthorizationState.EXECUTING); + + bool executed = output == int128(int256(SEEDED_Q)); + + if (!executed) { + vm.expectRevert( + abi.encodeWithSelector( + StandbyHook.StandbyHook__ProtectedOutputNotExecuted.selector, int256(output), SEEDED_Q + ) + ); + } + + serviceHarness.recordProtectedExecution( + exerciseRouter, servicePoolKey, _protectedExactOutputSwapParams(SEEDED_Q), toBalanceDelta(input, output) + ); + + assertEq( + uint256(_authorizationContext().state), + uint256( + executed + ? StandbyHook.ExerciseAuthorizationState.EXECUTED + : StandbyHook.ExerciseAuthorizationState.EXECUTING + ), + "only exactly the authorized protected output may establish execution" + ); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @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 + }) + ); + } +} + +/// @notice Fuzz evidence that a real authorized exercise produces exactly its authorized quantity +/// (G8B-9, G8B-13, G8B-14). +/// @dev The counterpart to the harness properties above, on the real stack and with nothing seeded: an +/// authentic commitment, the production authorization, the production execution coordination, and the +/// real pinned PoolManager. The quantity is fuzzed across the whole admissible extent, and each run +/// requires the currency that actually left the pool to equal it exactly, the F5 prediction to equal +/// the state the pool reached, and the commitment to be exactly as it was. +contract ProtectedExecutionQuantityFuzzTest is BaseExerciseAuthorizationTest { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev The canonical commitment entitlement: 50,000 MockUSDC. + uint128 internal constant CANONICAL_ENTITLEMENT = uint128(StandbyFixtureConfig.CANONICAL_COMMITMENT_Q); + + /*////////////////////////////////////////////////////////////// + EXACT REAL PROTECTED OUTPUT + //////////////////////////////////////////////////////////////*/ + + /// @notice A real authorized exercise produces exactly `q`, at every admissible quantity. + function testFuzz_authorizedExercise_producesExactlyQ(uint256 _qSeed) public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 q = _bound(_qSeed, 1, CANONICAL_ENTITLEMENT); + + uint256 predictedCapacity = hook.prospectiveSupportingCapacityAfterSwap(_protectedExactOutputSwapParams(q)); + + uint256 producedBefore = usdc.balanceOf(address(configuredExerciseRouter)); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, q); + + _assertExercisedContext( + commitmentId, commitmentExerciseAuthority, beneficiary, q, "every admissible quantity must execute exactly" + ); + + assertEq( + usdc.balanceOf(address(configuredExerciseRouter)) - producedBefore, + q, + "the pool must have produced exactly the authorized quantity" + ); + assertEq(hook.supportingCapacity(), predictedCapacity, "the actual post-state must be the predicted one"); + assertEq(_referenceSupportingCapacity(), predictedCapacity, "the independent reconstruction must agree"); + assertEq( + hook.commitment(commitmentId).remainingEntitlement, + CANONICAL_ENTITLEMENT, + "execution evidence must fulfil nothing" + ); + } +} diff --git a/test/harness/AdversarialExerciseRouter.sol b/test/harness/AdversarialExerciseRouter.sol new file mode 100644 index 0000000..8c79778 --- /dev/null +++ b/test/harness/AdversarialExerciseRouter.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +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 {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 {StandbyHook} from "../../src/StandbyHook.sol"; +import {IActorAwarePeriphery} from "../../src/interfaces/IActorAwarePeriphery.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice A hostile O2 coordinator, used as the configured ExerciseRouter of an adversarial service. +/// @dev The production `ExerciseRouter` proposes exactly one swap, of exactly the shape the Hook admits, at +/// exactly the right point in the operation. That is a property of the router, and Standby must not +/// depend on it: the configured ExerciseRouter is trusted for originating-user attribution and for +/// nothing else, so every classification restriction has to hold against a router that does its worst. +/// This contract is that router. It is configured as a real service's ExerciseRouter, it authenticates +/// like the real one, and it proposes whatever a test tells it to. +/// +/// It is deliberately not a subclass of the production router. Its purpose is to reach operation +/// shapes the production router structurally cannot express — a swap with no authorization behind it, +/// a substituted quantity, direction, price limit, or pool, a second swap against one authorization, +/// an extra unrelated pool interaction inside the same unlock, and a second authorization — so +/// inheriting production behavior would defeat the point. +/// +/// It closes the deltas of whatever it manages to execute, mechanically and from its own balance, for +/// the same reason the delta-closure router does and with the same absence of meaning: an operation +/// that Standby permits must be observable, and F8B settles nothing. Every refused operation unwinds +/// the unlock long before that closure could matter. +/// +/// Nothing this contract does is Standby behavior, and no test may read its ability to *attempt* +/// something as evidence that the attempt is admissible. +contract AdversarialExerciseRouter is IActorAwarePeriphery, IUnlockCallback { + /*////////////////////////////////////////////////////////////// + TYPE DECLARATIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice One hostile pool interaction sequence, carried across the unlock boundary. + /// @param key The pool the operations are performed against. + /// @param first The first proposed swap. + /// @param second The second proposed swap, performed only when `performSecond` is set. + /// @param performSecond Whether a second swap is attempted inside the same unlock. + /// @param hookData The payload forwarded to the Hook verbatim. + struct RoutedAttempt { + PoolKey key; + SwapParams first; + SwapParams second; + bool performSecond; + bytes hookData; + } + + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev The transient slot holding the originator of the request currently executing. + bytes32 private constant ACTOR_CONTEXT_SLOT = keccak256("standby.test.AdversarialExerciseRouter.actorContext"); + + StandbyHook public immutable i_hook; + IPoolManager public immutable i_poolManager; + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when the originator is queried while no request is executing. + error AdversarialExerciseRouter__NoActiveContext(); + + /// @notice Thrown when the unlock callback is invoked by anything other than the PoolManager. + /// @param caller The unauthorized caller. + error AdversarialExerciseRouter__NotPoolManager(address caller); + + /// @notice Thrown when the mechanical closure of a currency debt does not succeed. + /// @param currency The currency the debt is denominated in. + /// @param amount The raw amount owed. + error AdversarialExerciseRouter__ClosureTransferFailed(address currency, uint256 amount); + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /// @notice Binds the hostile coordinator to the Hook whose service it is configured on. + /// @param _hook The StandbyHook that owns the Protected Execution Service. + constructor(StandbyHook _hook) { + i_hook = _hook; + i_poolManager = _hook.poolManager(); + } + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Proposes a swap with no Hook authorization behind it. + /// @dev The configured ExerciseRouter identity, on the configured service pool, in the protected + /// direction, for a plausible quantity — and nothing else. If identity were classification, this + /// would be an O2 execution. + /// @param _key The pool to swap against. + /// @param _params The proposed swap. + /// @param _hookData The payload forwarded to the Hook verbatim. + function swapWithoutAuthorization(PoolKey calldata _key, SwapParams calldata _params, bytes calldata _hookData) + external + { + _begin(); + + _unlock(_key, _params, _params, false, _hookData); + + _end(); + } + + /// @notice Obtains a real authorization and then proposes an arbitrary swap against it. + /// @param _commitmentId The commitment to authorize. + /// @param _q The quantity to authorize. + /// @param _key The pool to swap against. + /// @param _params The proposed swap, which need not be the one the authorization admits. + /// @param _hookData The payload forwarded to the Hook verbatim. + function authorizeThenSwap( + uint256 _commitmentId, + uint256 _q, + PoolKey calldata _key, + SwapParams calldata _params, + bytes calldata _hookData + ) external { + _begin(); + + i_hook.authorizeExercise(_commitmentId, _q); + + _unlock(_key, _params, _params, false, _hookData); + + _end(); + } + + /// @notice Obtains one authorization and proposes two swaps against it inside one unlock. + /// @param _commitmentId The commitment to authorize. + /// @param _q The quantity to authorize. + /// @param _key The pool to swap against. + /// @param _first The first proposed swap. + /// @param _second The second proposed swap. + function authorizeThenSwapTwice( + uint256 _commitmentId, + uint256 _q, + PoolKey calldata _key, + SwapParams calldata _first, + SwapParams calldata _second + ) external { + _begin(); + + i_hook.authorizeExercise(_commitmentId, _q); + + _unlock(_key, _first, _second, true, bytes("")); + + _end(); + } + + /// @notice Attempts a second authorization before executing anything. + /// @param _commitmentId The commitment to authorize. + /// @param _firstQ The first authorized quantity. + /// @param _secondQ The second authorized quantity. + function authorizeTwice(uint256 _commitmentId, uint256 _firstQ, uint256 _secondQ) external { + _begin(); + + i_hook.authorizeExercise(_commitmentId, _firstQ); + i_hook.authorizeExercise(_commitmentId, _secondQ); + + _end(); + } + + /// @notice Performs the proposed hostile operations inside the PoolManager unlock. + /// @param _data The encoded attempt. + /// @return result The encoded balance delta of the last performed operation. + function unlockCallback(bytes calldata _data) external returns (bytes memory result) { + if (msg.sender != address(i_poolManager)) revert AdversarialExerciseRouter__NotPoolManager(msg.sender); + + RoutedAttempt memory attempt = abi.decode(_data, (RoutedAttempt)); + + BalanceDelta delta = i_poolManager.swap(attempt.key, attempt.first, attempt.hookData); + + if (attempt.performSecond) delta = delta + i_poolManager.swap(attempt.key, attempt.second, attempt.hookData); + + _closeDelta(attempt.key.currency0, delta.amount0()); + _closeDelta(attempt.key.currency1, delta.amount1()); + + result = abi.encode(delta); + } + + /// @inheritdoc IActorAwarePeriphery + function msgSender() external view returns (address actor) { + actor = _actorContext(); + + if (actor == address(0)) revert AdversarialExerciseRouter__NoActiveContext(); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Zeroes one currency's outstanding delta. Mechanical accounting closure, never settlement. + function _closeDelta(Currency _currency, int128 _amount) internal { + if (_amount < 0) { + uint256 owed = uint256(int256(-_amount)); + + i_poolManager.sync(_currency); + + bool paid = IERC20Minimal(Currency.unwrap(_currency)).transfer(address(i_poolManager), owed); + + if (!paid) revert AdversarialExerciseRouter__ClosureTransferFailed(Currency.unwrap(_currency), owed); + + i_poolManager.settle(); + } else if (_amount > 0) { + i_poolManager.take(_currency, address(this), uint256(int256(_amount))); + } + } + + /// @dev Opens the unlock carrying one hostile attempt. + function _unlock( + PoolKey calldata _key, + SwapParams memory _first, + SwapParams memory _second, + bool _performSecond, + bytes memory _hookData + ) internal { + i_poolManager.unlock( + abi.encode( + RoutedAttempt({ + key: _key, + first: _first, + second: _second, + performSecond: _performSecond, + hookData: _hookData + }) + ) + ); + } + + /// @dev Binds the caller as the originator of the request. + function _begin() internal { + _setActorContext(msg.sender); + } + + /// @dev Clears the originator. + function _end() internal { + _setActorContext(address(0)); + } + + /// @dev Writes the transient originator context. + function _setActorContext(address _actor) internal { + bytes32 slot = ACTOR_CONTEXT_SLOT; + + assembly ("memory-safe") { + tstore(slot, _actor) + } + } + + /// @dev Reads the transient originator context. + function _actorContext() internal view returns (address actor) { + bytes32 slot = ACTOR_CONTEXT_SLOT; + + assembly ("memory-safe") { + actor := tload(slot) + } + } +} diff --git a/test/harness/ExerciseDeltaClosureRouter.sol b/test/harness/ExerciseDeltaClosureRouter.sol new file mode 100644 index 0000000..c65e47a --- /dev/null +++ b/test/harness/ExerciseDeltaClosureRouter.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.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 {ExerciseRouter} from "../../src/ExerciseRouter.sol"; +import {StandbyHook} from "../../src/StandbyHook.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. +/// +/// 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. +/// +/// 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); +/// - 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); +/// - 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 { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when the mechanical closure of a currency debt does not succeed. + /// @param currency The currency the debt is denominated in. + /// @param amount The raw amount owed. + error ExerciseDeltaClosureRouter__ClosureTransferFailed(address currency, uint256 amount); + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /// @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) {} + + /*////////////////////////////////////////////////////////////// + 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. + /// @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(); + + _closeDelta(key.currency0, delta.amount0()); + _closeDelta(key.currency1, delta.amount1()); + + result = abi.encode(delta); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Zeroes one currency's outstanding delta. Mechanical accounting closure, never settlement. + function _closeDelta(Currency _currency, int128 _amount) internal { + if (_amount < 0) { + uint256 owed = uint256(int256(-_amount)); + + i_poolManager.sync(_currency); + + bool paid = IERC20Minimal(Currency.unwrap(_currency)).transfer(address(i_poolManager), owed); + + if (!paid) revert ExerciseDeltaClosureRouter__ClosureTransferFailed(Currency.unwrap(_currency), owed); + + i_poolManager.settle(); + } else if (_amount > 0) { + i_poolManager.take(_currency, address(this), uint256(int256(_amount))); + } + } +} diff --git a/test/harness/StandbyHookHarness.sol b/test/harness/StandbyHookHarness.sol index e307f14..09505c1 100644 --- a/test/harness/StandbyHookHarness.sol +++ b/test/harness/StandbyHookHarness.sol @@ -6,6 +6,9 @@ pragma solidity 0.8.26; //////////////////////////////////////////////////////////////*/ import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {SwapParams} from "v4-core/types/PoolOperation.sol"; import {StandbyHook} from "../../src/StandbyHook.sol"; @@ -18,8 +21,10 @@ import {StandbyHook} from "../../src/StandbyHook.sol"; /// @dev The mechanics under test are internal by design: F4 owns the storage primitives that commitment /// admission and fulfillment will later drive, and it deliberately introduces no production path /// that reaches them. F8A adds one more of the same kind — the claim of the single O2 authorization -/// slot, which no production caller can hold open on its own. Without this exposure the primitives -/// could not be verified at all before the slices that consume them exist, which would invert the +/// slot, which no production caller can hold open on its own. F8B adds three: writing a causal context +/// into a position production reaches only mid-swap, and driving the two O2 causal mechanics with +/// facts the real PoolManager would never produce. Without these exposures the primitives could not be +/// verified at all before the slices that consume them exist, which would invert the /// verification-gated dependency rule. /// /// Every function here is a bare pass-through. The harness declares no state of its own, adds no @@ -94,4 +99,48 @@ contract StandbyHookHarness is StandbyHook { function beginExerciseAuthorization() external { _beginExerciseAuthorization(); } + + /// @notice Writes a complete transaction-scoped causal context directly. + /// @dev Pure authority and nothing else: it authenticates nobody, evaluates no predicate, and asserts + /// nothing about backing. A context written here is not an authorization — it is a starting + /// position, and it exists so that the causal transitions guarding execution evidence can be + /// addressed one at a time. + /// + /// That is otherwise impossible. `EXECUTING` is reachable in production only from inside a + /// PoolManager swap the Hook has already accepted, which is precisely the situation in which the + /// evidence about to arrive cannot be malformed — the real PoolManager will not hand a Hook a + /// delta for a swap that did not happen. Verifying that malformed evidence is refused therefore + /// requires putting the context in that position without a real swap behind it. + /// @param _context The causal context to write. + function writeExerciseAuthorization(ExerciseAuthorizationContext memory _context) external { + _writeExerciseAuthorization(_context); + } + + /// @notice Runs the production O2 execution-classification mechanic. + /// @dev A bare pass-through of the `beforeSwap` O2 branch: it adds no check, removes none, and reaches + /// the same production causal transition. What it supplies is the ability to present a proposed + /// swap without the PoolManager having proposed it. + /// @param _sender The account presented as the PoolManager operation sender. + /// @param _params The proposed swap. + function beginProtectedExecution(address _sender, SwapParams calldata _params) external { + _beginProtectedExecution(_sender, _params); + } + + /// @notice Runs the production O2 execution-evidence mechanic. + /// @dev A bare pass-through of the `afterSwap` O2 branch, against production causal state. The delta it + /// is handed is supplied rather than produced by a real swap, which is the whole point: the + /// production path can only ever be handed an authentic PoolManager delta, so the refusal of an + /// inauthentic one is verifiable only here. + /// @param _sender The account presented as the PoolManager operation sender. + /// @param _key The pool the evidence claims to concern. + /// @param _params The swap the evidence claims to concern. + /// @param _delta The balance delta presented as execution evidence. + function recordProtectedExecution( + address _sender, + PoolKey calldata _key, + SwapParams calldata _params, + BalanceDelta _delta + ) external { + _recordProtectedExecution(_sender, _key, _params, _delta); + } } diff --git a/test/integration/ExerciseAuthorization.t.sol b/test/integration/ExerciseAuthorization.t.sol index e61dbcc..fff03a5 100644 --- a/test/integration/ExerciseAuthorization.t.sol +++ b/test/integration/ExerciseAuthorization.t.sol @@ -86,7 +86,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, @@ -102,13 +102,15 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { ); } - /// @notice Proves authorization alone fulfils nothing and changes no authoritative state. - /// @dev The whole F8A boundary, stated as one claim. After a successful authorization the commitment - /// record is byte-for-byte what it was, the derived obligation is what it was, Supporting Capacity - /// is what it was, the bounded index is what it was, no identity was consumed, and no protected - /// output reached the Beneficiary, the Hook, or the router. An authorization is a capability, not - /// a payment. - function test_successfulAuthorization_leavesEveryAuthoritativeFactUntouched() public { + /// @notice Proves a completed exercise request fulfils nothing and delivers nothing. + /// @dev The F8A boundary, stated against what a request now actually does. The request coordinates a + /// protected execution as well as an authorization, so the pool genuinely moves — that is the + /// exercise happening. What must not move is everything fulfillment would touch: the commitment + /// record is byte-for-byte what it was, Remaining Entitlement is what it was, the derived + /// obligation is what it was, the bounded index is what it was, no identity was consumed, and no + /// protected output reached the Beneficiary or the Hook. Authorization is a capability and + /// execution is evidence; neither is a payment. + function test_completedRequest_fulfilsNothingAndDeliversNothing() public { uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); StandbyHook.Commitment memory admitted = hook.commitment(commitmentId); @@ -116,48 +118,26 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertNoAdmissionResidue(before, "authorization must change no authoritative state"); - _assertCommitmentFactsUnchanged(commitmentId, admitted, "authorization must touch no commitment fact"); - _assertDerivationsMatchOracles("post-authorization derivations must equal their reconstructions"); + _assertCommitmentFactsUnchanged(commitmentId, admitted, "an exercise request must touch no commitment fact"); + _assertDerivationsMatchOracles("post-exercise derivations must equal their reconstructions"); + assertEq(hook.nextCommitmentId(), before.nextCommitmentId, "no commitment identity may be consumed"); assertEq( hook.commitment(commitmentId).remainingEntitlement, CANONICAL_ENTITLEMENT, - "authorization must not reduce Remaining Entitlement" + "no Remaining Entitlement may be reduced" ); assertEq( hook.aggregateObligation(), StandbyFixtureConfig.CANONICAL_COMMITMENT_Q, - "authorization must not reduce the authoritative obligation" + "no authoritative obligation may be reduced" ); - } - - /// @notice Proves the causal context does not advance beyond AUTHORIZED at this slice. - /// @dev An ordinary backed swap runs while an authorization is live. It is enforced as the ordinary O3 - /// transition it is, against the full unreduced obligation, and the causal context comes out of it - /// exactly as it went in. Nothing observes execution, nothing marks execution, and no entitlement - /// moves: `AUTHORIZED -> EXECUTED` does not exist yet. - function test_authorizedContext_doesNotAdvanceBeyondAuthorized() public { - uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); - - _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + assertEq(usdc.balanceOf(beneficiary), before.beneficiaryOutput, "the Beneficiary must receive nothing"); + assertEq(usdc.balanceOf(address(hook)), before.hookOutput, "the Hook must take custody of nothing"); - _swapAs(eligibleTrader, _protectedExactOutputSwapParams(StandbyFixtureConfig.COMPATIBLE_ORDINARY_SWAP_OUTPUT)); + (bool referenced,) = _referenceSlotOf(commitmentId); - _assertAuthorizationContext( - commitmentId, - commitmentExerciseAuthority, - beneficiary, - EXERCISE_Q, - "an ordinary transition may not advance or consume the causal context" - ); - - assertEq(hook.supportingCapacity(), StandbyFixtureConfig.EXPECTED_A2_S, "the ordinary swap was ordinary"); - assertEq( - hook.aggregateObligation(), - StandbyFixtureConfig.CANONICAL_COMMITMENT_Q, - "no fulfillment may be attributed to an authorization" - ); + assertTrue(referenced, "the bounded reference must survive an exercise that fulfils nothing"); } /*////////////////////////////////////////////////////////////// @@ -217,7 +197,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(secondAuthority, second, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( second, secondAuthority, secondBeneficiary, EXERCISE_Q, "each authority may authorize its own commitment" ); } @@ -264,7 +244,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, EXERCISE_Q, "exercisableFrom is inclusive" ); } @@ -279,7 +259,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, EXERCISE_Q, "validUntil is exclusive" ); } @@ -373,7 +353,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, EXERCISE_Q, "restored eligibility must authorize" ); } @@ -404,7 +384,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, EXERCISE_Q, "0 < q < Remaining is permitted" ); } @@ -417,7 +397,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, CANONICAL_ENTITLEMENT); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, CANONICAL_ENTITLEMENT, "q == Remaining is permitted" ); } @@ -461,7 +441,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, EXERCISE_Q, "S' > O - q must be authorized" ); } @@ -490,7 +470,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, EXERCISE_Q, "S' == O - q must be authorized" ); @@ -534,7 +514,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { vm.expectRevert(StandbyHook.StandbyHook__ExerciseAuthorizationAlreadyActive.selector); _authorizeAs(secondAuthority, second, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( first, commitmentExerciseAuthority, beneficiary, @@ -587,7 +567,7 @@ contract ExerciseAuthorizationTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, @@ -626,7 +606,7 @@ contract ExerciseAuthorizationTransactionScopeTest is BaseExerciseAuthorizationT _authorizeAs(commitmentExerciseAuthority, carriedCommitmentId, CARRIED_Q); - _assertAuthorizationContext( + _assertExercisedContext( carriedCommitmentId, commitmentExerciseAuthority, beneficiary, @@ -650,7 +630,7 @@ contract ExerciseAuthorizationTransactionScopeTest is BaseExerciseAuthorizationT function test_theNextTransaction_mayAuthorizeAfresh() public { _authorizeAs(commitmentExerciseAuthority, carriedCommitmentId, CARRIED_Q); - _assertAuthorizationContext( + _assertExercisedContext( carriedCommitmentId, commitmentExerciseAuthority, beneficiary, diff --git a/test/integration/ProtectedExecution.t.sol b/test/integration/ProtectedExecution.t.sol new file mode 100644 index 0000000..59f863d --- /dev/null +++ b/test/integration/ProtectedExecution.t.sol @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {IHooks} from "v4-core/interfaces/IHooks.sol"; + +import {StandbyFixtureConfig} from "../../script/helpers/StandbyFixtureConfig.sol"; + +import {StandbyHook} from "../../src/StandbyHook.sol"; + +import {BaseExerciseAuthorizationTest} from "../shared/BaseExerciseAuthorizationTest.t.sol"; +import {BaseUnbackedExerciseAuthorizationTest} from "../shared/BaseUnbackedExerciseAuthorizationTest.t.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Integration evidence for F8B O2 execution classification and execution evidence on the real +/// execution stack (G8B-1, G8B-3 through G8B-5, G8B-8 through G8B-11, G8B-13, G8B-14, G8B-18). +/// @dev Every execution here is performed by the real pinned `PoolManager` against a real pool holding real +/// liquidity, for a commitment the production O1 transition admitted, through the production +/// authorization and the production execution coordination. Nothing about the classification decision +/// or the execution evidence is seeded. +/// +/// One thing is not production: the deltas the swap opens are closed mechanically by the fixture's +/// delta-closure router, because input settlement and Beneficiary delivery are F8C's and no production +/// path can close them yet. That closure is accounting plumbing and carries no economics — which these +/// tests assert rather than assume, by checking that the Beneficiary receives nothing, that the Hook +/// takes custody of nothing, and that no Remaining Entitlement or obligation moves. +/// +/// The predicted-versus-actual evidence deliberately compares three quantities that were produced +/// three different ways: the F5 prospective derivation's prediction, taken before the execution; the +/// production derivation over the pool the execution actually left; and the independent +/// `ReferenceCalculations` reconstruction over that same actual state. A prediction that were merely +/// close would authorize exercises against a state that never existed. +contract ProtectedExecutionTest is BaseExerciseAuthorizationTest { + /*////////////////////////////////////////////////////////////// + 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; + + /// @dev The Supporting Capacity the canonical exercise leaves: 80,000 - 20,000 MockUSDC. + uint256 internal constant EXPECTED_POST_EXECUTION_S = 60_000_000_000; + + /*////////////////////////////////////////////////////////////// + G8B-1, G8B-8, G8B-9 — AUTHORIZED EXECUTION + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves an authorized exercise executes exactly `q` and records exactly that as evidence. + /// @dev The canonical positive case, end to end on the real stack. The causal context reaches + /// `EXECUTED` with every binding authorization resolved still intact, and the pool genuinely + /// produced `q` of the protected output currency — which is checked against the currency that + /// actually left the PoolManager rather than against the amount that was requested. + function test_authorizedExercise_executesExactlyQAndRecordsEvidence() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 producedBefore = usdc.balanceOf(address(configuredExerciseRouter)); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _assertExercisedContext( + commitmentId, + commitmentExerciseAuthority, + beneficiary, + EXERCISE_Q, + "the executed context must carry exactly the authorized bindings" + ); + + assertEq( + usdc.balanceOf(address(configuredExerciseRouter)) - producedBefore, + EXERCISE_Q, + "the pool must actually have produced exactly the authorized protected output" + ); + } + + /// @notice Proves execution evidence changes no commitment state and delivers nothing. + /// @dev `EXECUTED` means the authorized swap happened, and deliberately nothing more. The input debt + /// was closed by test plumbing rather than settled, no cost bound was enforced, the Beneficiary + /// received nothing, Remaining Entitlement is untouched, and the authoritative obligation is + /// exactly what it was — so nothing here may be read as settlement, delivery, or fulfillment. + function test_executionEvidence_fulfilsNothing() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + StandbyHook.Commitment memory admitted = hook.commitment(commitmentId); + + uint256 beneficiaryBefore = usdc.balanceOf(beneficiary); + uint256 hookBefore = usdc.balanceOf(address(hook)); + uint256 obligationBefore = hook.aggregateObligation(); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _assertCommitmentFactsUnchanged(commitmentId, admitted, "execution must touch no commitment fact"); + + assertEq( + hook.commitment(commitmentId).remainingEntitlement, + CANONICAL_ENTITLEMENT, + "execution evidence must not reduce Remaining Entitlement" + ); + assertEq(hook.aggregateObligation(), obligationBefore, "execution evidence must not reduce the obligation"); + assertEq(usdc.balanceOf(beneficiary), beneficiaryBefore, "execution evidence is not Beneficiary delivery"); + assertEq(usdc.balanceOf(address(hook)), hookBefore, "the Hook must take custody of no protected output"); + } + + /*////////////////////////////////////////////////////////////// + G8B-13 — PREDICTED VERSUS ACTUAL + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves the F5 prediction the authorization was decided on is the state the pool reached. + /// @dev Three independently produced quantities are required to agree: the prospective Supporting + /// Capacity derived before the execution from the canonical protected execution, the production + /// derivation over the pool the execution actually left, and the independent reconstruction over + /// that same actual state. The frozen fixture expectation pins the value itself, so agreement + /// cannot be agreement on a wrong number. + /// + /// Active liquidity is asserted unchanged as well, because equal capacity at unequal liquidity + /// would mean the price landed somewhere else. + function test_executedExercise_reachesTheF5PredictedPostState() public { + _establishExercisable(CANONICAL_ENTITLEMENT); + + uint256 predictedCapacity = + hook.prospectiveSupportingCapacityAfterSwap(_protectedExactOutputSwapParams(EXERCISE_Q)); + + (,, uint128 liquidityBefore) = _servicePoolState(); + + assertEq(predictedCapacity, EXPECTED_POST_EXECUTION_S, "the prediction must be the frozen expectation"); + + _authorizeAs(commitmentExerciseAuthority, 1, EXERCISE_Q); + + (uint160 sqrtPriceAfter,, uint128 liquidityAfter) = _servicePoolState(); + + assertEq(hook.supportingCapacity(), predictedCapacity, "the actual post-state must be the predicted one"); + assertEq( + _referenceSupportingCapacity(), + predictedCapacity, + "the independent reconstruction of the actual post-state must agree" + ); + assertEq(liquidityAfter, liquidityBefore, "the execution must not have crossed a liquidity boundary"); + assertLt(sqrtPriceAfter, uint160(1) << 96, "the protected execution must have moved the price down"); + } + + /*////////////////////////////////////////////////////////////// + G8B-4, G8B-18 — O2 / O3 EXCLUSION + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves ordinary swaps keep their ordinary O3 treatment when no O2 operation is in progress. + /// @dev Both halves, against an authentic positive obligation: a backed ordinary swap is admitted, and + /// a backing-destructive one is refused by the ordinary comparison rather than by anything the + /// execution classifier introduced. Existence of an O2 path may not weaken O3. + function test_ordinarySwaps_keepTheirO3TreatmentWithoutAnO2Context() public { + _establishExercisable(CANONICAL_ENTITLEMENT); + + _swapAs(eligibleTrader, _protectedExactOutputSwapParams(StandbyFixtureConfig.COMPATIBLE_ORDINARY_SWAP_OUTPUT)); + + assertEq(hook.supportingCapacity(), StandbyFixtureConfig.EXPECTED_A2_S, "the ordinary swap was ordinary"); + + _expectBackingRejection( + IHooks.beforeSwap.selector, + StandbyFixtureConfig.EXPECTED_A2_S - StandbyFixtureConfig.DESTRUCTIVE_SWAP_OUTPUT_ATTEMPT, + StandbyFixtureConfig.CANONICAL_COMMITMENT_Q + ); + _swapAs(eligibleTrader, _protectedExactOutputSwapParams(StandbyFixtureConfig.DESTRUCTIVE_SWAP_OUTPUT_ATTEMPT)); + } + + /// @notice Proves an ordinary swap cannot proceed while an O2 causal context is unresolved. + /// @dev The swap used here is one the ordinary O3 rule would admit without hesitation — a backed, + /// eligible, correctly routed protected swap. It is refused anyway, because the service is inside + /// an O2 operation whose authorization was decided against `S' >= O - q` rather than `S' >= O`. + /// Letting it through would enforce the wrong rule against the wrong state. + function test_ordinarySwap_whileAnO2ContextIsUnresolved_isRejected() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _expectHookRejection( + IHooks.beforeSwap.selector, + abi.encodeWithSelector( + StandbyHook.StandbyHook__ExerciseExecutionNotAuthorized.selector, + StandbyHook.ExerciseAuthorizationState.EXECUTED + ) + ); + _swapAs(eligibleTrader, _protectedExactOutputSwapParams(StandbyFixtureConfig.COMPATIBLE_ORDINARY_SWAP_OUTPUT)); + } + + /*////////////////////////////////////////////////////////////// + G8B-11 — EXACTLY ONE EXERCISE + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves execution evidence cannot be reused by a second exercise in the same transaction. + /// @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, which is the single + /// restriction that covers a second authorization, a second execution, and an overwrite alike. + function test_secondExercise_againstAnUnresolvedContext_isRejected() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + vm.expectRevert(StandbyHook.StandbyHook__ExerciseAuthorizationAlreadyActive.selector); + _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); + + _assertExercisedContext( + commitmentId, + commitmentExerciseAuthority, + beneficiary, + EXERCISE_Q, + "a refused second exercise must not disturb the standing evidence" + ); + } + + /*////////////////////////////////////////////////////////////// + G8B-9 — EXACT EXECUTION EXTENT + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves an exercise of exactly the Supporting Capacity executes exactly. + /// @dev The boundary the qualification price limit creates. An exact-output execution for the whole of + /// Supporting Capacity lands exactly on `P_Q` and produces exactly `q`, leaving no capacity behind + /// — which is an ordinary valid state, not a failure. One raw unit more would be undeliverable, + /// and the fuzzed backing evidence covers that side. + function test_exerciseOfTheWholeSupportingCapacity_executesExactly() public { + uint128 entitlement = uint128(StandbyFixtureConfig.EXPECTED_INITIAL_S); + + uint256 commitmentId = _establishExercisable(entitlement); + + uint256 producedBefore = usdc.balanceOf(address(configuredExerciseRouter)); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, entitlement); + + _assertExercisedContext( + commitmentId, commitmentExerciseAuthority, beneficiary, entitlement, "exact capacity must execute exactly" + ); + + assertEq( + usdc.balanceOf(address(configuredExerciseRouter)) - producedBefore, + uint256(entitlement), + "the pool must have produced the whole authorized quantity" + ); + assertEq(hook.supportingCapacity(), 0, "the execution must land exactly on the qualification boundary"); + } +} + +/// @notice Integration evidence that a failed protected execution unwinds the whole exercise (G8B-9, +/// G8B-10, G8B-12). +/// @dev The failing execution has to be a real one, and reaching it requires a state the production system +/// maintains against: a commitment whose Remaining Entitlement exceeds what the pool can deliver +/// before `P_Q`. `S >= O >= q` holds on every reachable path, so the remainder is harness-written — +/// the same harness contribution the F8A backing evidence already rests on — and everything else is +/// authentic: the pool, the liquidity, the commitment, the authorization, the router, and the swap. +/// +/// What that buys is the one case RR-O2-9 legislates for. The authorization passes, the PoolManager +/// performs the swap, and the swap produces everything the domain has rather than the `q` that was +/// requested. A partial individual exercise is a failure of the complete O2, so the containing +/// transaction reverts — and with it the authorization, the execution marker, and the swap itself. +contract ProtectedExecutionFailureTest is BaseUnbackedExerciseAuthorizationTest { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev An authentically admissible entitlement: 20,000 MockUSDC. + uint128 internal constant ADMITTED_ENTITLEMENT = 20_000_000_000; + + /// @dev A remainder the canonical pool cannot deliver: 120,000 MockUSDC against 80,000 of capacity. + uint128 internal constant UNDELIVERABLE_REMAINING = 120_000_000_000; + + /*////////////////////////////////////////////////////////////// + FAILED EXECUTION ATOMICITY + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves a partial actual output cannot produce execution evidence. + /// @dev The request is for `q` and the pool produces less, which Uniswap treats as an ordinary + /// successful swap. Standby does not: the evidence comparison is exact, so the swap is refused + /// after it has already executed and the PoolManager unwinds it. + function test_partialActualOutput_cannotProduceExecutionEvidence() public { + uint256 commitmentId = _establishExercisable(ADMITTED_ENTITLEMENT); + + _writeRemainingEntitlement(commitmentId, UNDELIVERABLE_REMAINING); + + uint256 deliverable = _referenceSupportingCapacity(); + + assertLt(deliverable, UNDELIVERABLE_REMAINING, "the fixture must be asking for more than the pool holds"); + + _expectHookRejection( + IHooks.afterSwap.selector, + abi.encodeWithSelector( + StandbyHook.StandbyHook__ProtectedOutputNotExecuted.selector, + int256(deliverable), + uint256(UNDELIVERABLE_REMAINING) + ) + ); + _authorizeAs(commitmentExerciseAuthority, commitmentId, UNDELIVERABLE_REMAINING); + } + + /// @notice Proves a failed protected execution leaves nothing behind and cannot be continued. + /// @dev The production ExerciseRouter has no failure branch: the PoolManager call is not caught, so a + /// failed execution propagates out of `exercise` and unwinds the authorization that preceded it + /// along with the swap. That matters because the execution marker is transaction-scoped — a router + /// that caught the failure would find the context restored to AUTHORIZED and could execute against + /// it again. This one cannot: after the failure there is no context at all, the pool is exactly + /// where it was, and the next exercise in the very same transaction is a fresh authorization + /// subject to every predicate again. + function test_failedExecution_unwindsTheExerciseAndCannotBeRetried() public { + uint256 commitmentId = _establishExercisable(ADMITTED_ENTITLEMENT); + + _writeRemainingEntitlement(commitmentId, UNDELIVERABLE_REMAINING); + + (uint160 sqrtPriceBefore, int24 tickBefore, uint128 liquidityBefore) = _servicePoolState(); + + vm.expectRevert(); + _authorizeAs(commitmentExerciseAuthority, commitmentId, UNDELIVERABLE_REMAINING); + + _assertNoAuthorizationContext("a failed execution must leave no causal context"); + + (uint160 sqrtPriceAfter, int24 tickAfter, uint128 liquidityAfter) = _servicePoolState(); + + assertEq(sqrtPriceAfter, sqrtPriceBefore, "a failed execution must leave the pool price untouched"); + assertEq(tickAfter, tickBefore, "a failed execution must leave the pool tick untouched"); + assertEq(liquidityAfter, liquidityBefore, "a failed execution must leave active liquidity untouched"); + assertEq( + hook.commitment(commitmentId).remainingEntitlement, + UNDELIVERABLE_REMAINING, + "a failed execution must fulfil nothing" + ); + + vm.expectRevert(); + _authorizeAs(commitmentExerciseAuthority, commitmentId, UNDELIVERABLE_REMAINING); + + _assertNoAuthorizationContext("a repeated attempt must gain nothing from the failed one"); + + _writeRemainingEntitlement(commitmentId, ADMITTED_ENTITLEMENT); + + _authorizeAs(commitmentExerciseAuthority, commitmentId, ADMITTED_ENTITLEMENT); + + _assertExercisedContext( + commitmentId, + commitmentExerciseAuthority, + beneficiary, + ADMITTED_ENTITLEMENT, + "the next exercise must be a fresh authorization rather than a continuation" + ); + } +} diff --git a/test/periphery/ExerciseAuthorizationPerimeter.t.sol b/test/periphery/ExerciseAuthorizationPerimeter.t.sol index 0f98a70..2dcc231 100644 --- a/test/periphery/ExerciseAuthorizationPerimeter.t.sol +++ b/test/periphery/ExerciseAuthorizationPerimeter.t.sol @@ -140,7 +140,7 @@ contract ExerciseAuthorizationPerimeterTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, @@ -166,7 +166,7 @@ contract ExerciseAuthorizationPerimeterTest is BaseExerciseAuthorizationTest { _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, EXERCISE_Q, "the authority itself may authorize" ); } @@ -189,7 +189,7 @@ contract ExerciseAuthorizationPerimeterTest is BaseExerciseAuthorizationTest { vm.prank(commitmentExerciseAuthority, unauthorizedExerciser); configuredExerciseRouter.exercise(commitmentId, EXERCISE_Q, UNCONSTRAINED_MAX_INPUT); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, @@ -282,8 +282,9 @@ contract ExerciseAuthorizationPerimeterTest is BaseExerciseAuthorizationTest { /// @notice Proves a `maxInput` of zero authorizes exactly what any other value authorizes. /// @dev Zero is the value most likely to be mistaken for a constraint — it would refuse every real /// exercise if anything enforced it — and it changes nothing here. `maxInput` is the exerciser's - /// own cost bound against an authoritative input debt, and there is no debt at this slice, so the - /// field reaches no code and no decision. + /// own cost bound against the authoritative input debt the executed swap produces, and enforcing + /// it belongs to F8C together with settling that debt, so the field reaches no code and no + /// decision. function test_maxInputOfZero_authorizesIdentically() public { uint256 commitmentId = _establishExercisable(EXERCISE_ENTITLEMENT); @@ -355,7 +356,7 @@ contract ExerciseAuthorizationPerimeterTest is BaseExerciseAuthorizationTest { vm.expectRevert(ExerciseRouter.ExerciseRouter__NoActiveExerciseContext.selector); configuredExerciseRouter.msgSender(); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, @@ -375,7 +376,7 @@ contract ExerciseAuthorizationPerimeterTest is BaseExerciseAuthorizationTest { /// and the commitment's own remainder. A `maxInput` that shifted any of them — a different `q`, a /// different Beneficiary, a different attribution, a different `S` or `O` — would show up here. function _assertMaxInputIndependentAuthorization(uint256 _commitmentId) internal view { - _assertAuthorizationContext( + _assertExercisedContext( _commitmentId, commitmentExerciseAuthority, beneficiary, diff --git a/test/periphery/ProtectedExecutionPerimeter.t.sol b/test/periphery/ProtectedExecutionPerimeter.t.sol new file mode 100644 index 0000000..46a12a6 --- /dev/null +++ b/test/periphery/ProtectedExecutionPerimeter.t.sol @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {TickMath} from "v4-core/libraries/TickMath.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolIdLibrary} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {SwapParams} from "v4-core/types/PoolOperation.sol"; + +import {StandbyFixtureConfig} from "../../script/helpers/StandbyFixtureConfig.sol"; + +import {StandbyHook} from "../../src/StandbyHook.sol"; + +import {BaseAdversarialExerciseTest} from "../shared/BaseAdversarialExerciseTest.t.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Periphery evidence that O2 classification survives a hostile configured ExerciseRouter (G8B-1 +/// through G8B-4, G8B-7, G8B-11). +/// @dev The service here is activated with `AdversarialExerciseRouter` in the configured O2 coordinator +/// role. It is a real configured router with real originating-user attribution, and it proposes +/// operations the production router structurally cannot: a swap with no authorization behind it, a +/// substituted quantity, direction, price limit or pool, two swaps against one authorization, and a +/// second authorization. +/// +/// What every test below establishes is the same thing from a different angle: the configured router +/// is trusted for exactly one fact — who originated the request — and acquires no ability to classify +/// an execution, manufacture evidence, or reuse an authorization by being the configured router. +/// Classification is conjunctive, and each conjunct is falsified separately here. +contract ProtectedExecutionPerimeterTest is BaseAdversarialExerciseTest { + using PoolIdLibrary for PoolKey; + + /*////////////////////////////////////////////////////////////// + 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; + + /// @dev A payload shaped to imitate a Standby causal context. + bytes internal forgedCausalPayload; + + /*////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + + /// @notice Prepares a payload that names everything an authorization binds. + function setUp() public virtual override { + super.setUp(); + + forgedCausalPayload = abi.encode( + servicePoolId, + uint256(1), + address(adversarialExerciseRouter), + commitmentExerciseAuthority, + beneficiary, + EXERCISE_Q + ); + } + + /*////////////////////////////////////////////////////////////// + G8B-1, G8B-2 — AUTHORIZATION REQUIRED + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves the configured router's identity alone classifies nothing. + /// @dev The proposal is exactly the shape an authorized exercise takes — the configured service pool, + /// the protected direction, exact output, a plausible quantity, the qualification boundary — from + /// exactly the configured ExerciseRouter. With no authorization behind it there is no O2 operation + /// in progress, so the Hook treats it as the ordinary swap it is and refuses it at the ordinary + /// perimeter, which the ExerciseRouter is not. + function test_configuredRouterIdentityAlone_doesNotClassifyO2() public { + _establishExercisable(CANONICAL_ENTITLEMENT); + + _expectHookRejection( + IHooks.beforeSwap.selector, + abi.encodeWithSelector( + StandbyHook.StandbyHook__UntrustedSwapPerimeter.selector, address(adversarialExerciseRouter) + ) + ); + adversarialExerciseRouter.swapWithoutAuthorization( + servicePoolKey, _protectedExactOutputSwapParams(EXERCISE_Q), bytes("") + ); + + _assertNoAuthorizationContext("an unauthorized swap must create no causal context"); + } + + /// @notice Proves a payload that imitates a causal context manufactures nothing. + /// @dev The Hook never reads `hookData`, and the point of asserting it is that a payload naming the + /// service, a commitment, the configured router, the exercise authority, the Beneficiary, and a + /// quantity changes no outcome at all — the refusal is byte-for-byte the one the empty payload + /// produced. + function test_forgedCausalPayload_manufacturesNoAuthorization() public { + _establishExercisable(CANONICAL_ENTITLEMENT); + + _expectHookRejection( + IHooks.beforeSwap.selector, + abi.encodeWithSelector( + StandbyHook.StandbyHook__UntrustedSwapPerimeter.selector, address(adversarialExerciseRouter) + ) + ); + adversarialExerciseRouter.swapWithoutAuthorization( + servicePoolKey, _protectedExactOutputSwapParams(EXERCISE_Q), forgedCausalPayload + ); + + _assertNoAuthorizationContext("a forged payload must create no causal context"); + } + + /*////////////////////////////////////////////////////////////// + G8B-3 — EXACT OPERATION IDENTITY + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves an authorized exercise of exactly the admitted shape is classified O2. + /// @dev The positive control for every rejection below, and the reason they are attributable. The same + /// hostile router, the same authorization, and the one operation the Hook admits: it executes, and + /// the resulting evidence carries the bindings the authorization resolved. Classification is a + /// property of the operation rather than of the contract proposing it. + function test_authorizedExactOperation_isClassifiedO2() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + vm.prank(commitmentExerciseAuthority); + adversarialExerciseRouter.authorizeThenSwap( + commitmentId, EXERCISE_Q, servicePoolKey, _protectedExactOutputSwapParams(EXERCISE_Q), bytes("") + ); + + _assertExercisedContext( + commitmentId, + commitmentExerciseAuthority, + beneficiary, + EXERCISE_Q, + "the exact admitted operation must be classified as the O2 execution" + ); + } + + /// @notice Proves a forged payload changes nothing about an operation that is genuinely authorized. + /// @dev The complement of the manufacture case. `hookData` cannot create classification, and it cannot + /// disturb it either: the same admitted operation with an imitative payload executes identically. + function test_forgedCausalPayload_changesNothingAboutAnAuthorizedExecution() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + vm.prank(commitmentExerciseAuthority); + adversarialExerciseRouter.authorizeThenSwap( + commitmentId, EXERCISE_Q, servicePoolKey, _protectedExactOutputSwapParams(EXERCISE_Q), forgedCausalPayload + ); + + _assertExercisedContext( + commitmentId, commitmentExerciseAuthority, beneficiary, EXERCISE_Q, "hookData may not change an outcome" + ); + } + + /// @notice Proves a substituted quantity cannot satisfy the authorization. + /// @dev One raw unit either side, and nothing at all. Each is a different operation from the one the + /// authorization admits, and the exercise it would perform is not the exercise that was decided. + function test_substitutedQuantity_isRejected() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + _expectExecutionMismatch(_protectedExactOutputSwapParams(EXERCISE_Q - 1)); + _authorizeThenSwap(commitmentId, _protectedExactOutputSwapParams(EXERCISE_Q - 1)); + + _expectExecutionMismatch(_protectedExactOutputSwapParams(EXERCISE_Q + 1)); + _authorizeThenSwap(commitmentId, _protectedExactOutputSwapParams(EXERCISE_Q + 1)); + } + + /// @notice Proves an exact-input swap cannot satisfy an exact-output authorization. + /// @dev Under the pinned v4 convention a negative `amountSpecified` names a desired input, so the same + /// magnitude in the same direction at the same boundary is still a different operation: it fixes + /// what the exerciser spends instead of what the Beneficiary is owed. + function test_exactInputSubstitution_isRejected() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + SwapParams memory exactInput = _protectedSwapParams(EXERCISE_Q); + + _expectExecutionMismatch(exactInput); + _authorizeThenSwap(commitmentId, exactInput); + } + + /// @notice Proves the opposite direction cannot satisfy the authorization. + function test_substitutedDirection_isRejected() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + SwapParams memory opposite = _swapParams(false, int256(EXERCISE_Q), StandbyFixtureConfig.TICK_O); + + _expectExecutionMismatch(opposite); + _authorizeThenSwap(commitmentId, opposite); + } + + /// @notice Proves a substituted qualification boundary cannot satisfy the authorization. + /// @dev Everything else matches: the pool, the direction, exact output, the exact quantity. The price + /// limit is the service's own execution-quality boundary, and an exercise executed against a + /// different one is not the exercise the service promised. + function test_substitutedQualificationBoundary_isRejected() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + SwapParams memory shiftedBoundary = + _swapParams(true, int256(EXERCISE_Q), StandbyFixtureConfig.TICK_Q + StandbyFixtureConfig.TICK_SPACING); + + _expectExecutionMismatch(shiftedBoundary); + _authorizeThenSwap(commitmentId, shiftedBoundary); + } + + /// @notice Proves no other pool can carry the authorized execution. + /// @dev A Hook address encodes its permissions, so anyone may bind this Hook to another pool. That pool + /// is not this service, and an authorization for this service cannot be executed against it. + function test_substitutedPool_isRejected() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + PoolKey memory foreignKey = PoolKey({ + currency0: Currency.wrap(address(ustb)), + currency1: Currency.wrap(address(usdc)), + fee: 3000, + tickSpacing: 60, + hooks: IHooks(address(hook)) + }); + + poolManager.initialize(foreignKey, TickMath.getSqrtPriceAtTick(0)); + + _expectHookRejection( + IHooks.beforeSwap.selector, + abi.encodeWithSelector(StandbyHook.StandbyHook__PoolIsNotConfiguredService.selector, foreignKey.toId()) + ); + + vm.prank(commitmentExerciseAuthority); + adversarialExerciseRouter.authorizeThenSwap( + commitmentId, EXERCISE_Q, foreignKey, _protectedExactOutputSwapParams(EXERCISE_Q), bytes("") + ); + } + + /*////////////////////////////////////////////////////////////// + G8B-11 — EXACTLY ONE SWAP + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves one authorization admits exactly one swap, whatever the second one looks like. + /// @dev Both shapes of second interaction inside the same unlock: a repeat of the admitted execution, + /// and an unrelated ordinary swap. Neither may proceed, because the causal context is no longer + /// AUTHORIZED once its execution has happened — a second execution and a nested unrelated + /// interaction are the same refusal. + function test_secondSwapInsideOneAuthorization_isRejected() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + SwapParams memory admitted = _protectedExactOutputSwapParams(EXERCISE_Q); + + _expectExecutedContextRejection(); + + vm.prank(commitmentExerciseAuthority); + adversarialExerciseRouter.authorizeThenSwapTwice(commitmentId, EXERCISE_Q, servicePoolKey, admitted, admitted); + + _expectExecutedContextRejection(); + + vm.prank(commitmentExerciseAuthority); + adversarialExerciseRouter.authorizeThenSwapTwice( + commitmentId, EXERCISE_Q, servicePoolKey, admitted, _protectedSwapParams(1_000_000_000) + ); + } + + /// @notice Proves a second authorization cannot coexist with an unresolved one. + /// @dev Attempted before anything executes, so the refusal is about the causal context alone rather + /// than about execution evidence. + function test_secondAuthorization_isRejected() public { + uint256 commitmentId = _establishExercisable(CANONICAL_ENTITLEMENT); + + vm.expectRevert(StandbyHook.StandbyHook__ExerciseAuthorizationAlreadyActive.selector); + + vm.prank(commitmentExerciseAuthority); + adversarialExerciseRouter.authorizeTwice(commitmentId, EXERCISE_Q, EXERCISE_Q); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev The context router of this fixture is the hostile coordinator the service was activated with. + function _expectedContextRouter() internal view override returns (address router) { + router = address(adversarialExerciseRouter); + } + + /// @dev Authorizes the canonical quantity and then proposes some other operation against it. + function _authorizeThenSwap(uint256 _commitmentId, SwapParams memory _params) internal { + vm.prank(commitmentExerciseAuthority); + adversarialExerciseRouter.authorizeThenSwap(_commitmentId, EXERCISE_Q, servicePoolKey, _params, bytes("")); + } + + /// @dev Expects the refusal of an operation that is not the one the authorization admits. + function _expectExecutionMismatch(SwapParams memory _params) internal { + _expectHookRejection( + IHooks.beforeSwap.selector, + abi.encodeWithSelector( + StandbyHook.StandbyHook__NotTheAuthorizedProtectedExecution.selector, + _params.zeroForOne, + _params.amountSpecified, + _params.sqrtPriceLimitX96 + ) + ); + } + + /// @dev Expects the refusal of any swap proposed while an execution has already happened. + function _expectExecutedContextRejection() internal { + _expectHookRejection( + IHooks.beforeSwap.selector, + abi.encodeWithSelector( + StandbyHook.StandbyHook__ExerciseExecutionNotAuthorized.selector, + StandbyHook.ExerciseAuthorizationState.EXECUTED + ) + ); + } +} diff --git a/test/shared/BaseAdversarialExerciseTest.t.sol b/test/shared/BaseAdversarialExerciseTest.t.sol new file mode 100644 index 0000000..5eb5056 --- /dev/null +++ b/test/shared/BaseAdversarialExerciseTest.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {AdversarialExerciseRouter} from "../harness/AdversarialExerciseRouter.sol"; +import {BaseExerciseAuthorizationTest} from "./BaseExerciseAuthorizationTest.t.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Shared fixture whose configured ExerciseRouter is hostile. +/// @dev The production ExerciseRouter proposes one swap, of the admitted shape, at the right moment. Every +/// Standby classification restriction has to hold without that cooperation, because the configured +/// ExerciseRouter is trusted for originating-user attribution and for nothing else. This fixture +/// therefore activates the canonical service with `AdversarialExerciseRouter` in that role — a real +/// configured coordinator, authenticated exactly as the production one is, that proposes whatever a +/// test asks it to. +/// +/// Everything else is the ordinary F8A/F8B fixture and is unchanged: the real PoolManager, the +/// production Hook from the canonical deployment procedure, the production activation, the canonical +/// liquidity through the production liquidity path, and commitments created only by the production O1 +/// transition. Nothing about the Hook is substituted, so every refusal observed here is a production +/// refusal. +/// +/// The production `ExerciseRouter` is not exercised by this fixture and no claim about it may be drawn +/// from one: what is proven here is that the Hook does not depend on it. +abstract contract BaseAdversarialExerciseTest is BaseExerciseAuthorizationTest { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev The hostile coordinator the service was actually activated with. + AdversarialExerciseRouter internal adversarialExerciseRouter; + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev Activates the service with the hostile coordinator instead of the production router. + function _resolveExerciseRouter() internal override returns (address router) { + adversarialExerciseRouter = new AdversarialExerciseRouter(hook); + + router = address(adversarialExerciseRouter); + } +} diff --git a/test/shared/BaseExerciseAuthorizationTest.t.sol b/test/shared/BaseExerciseAuthorizationTest.t.sol index 157fb3c..30073e1 100644 --- a/test/shared/BaseExerciseAuthorizationTest.t.sol +++ b/test/shared/BaseExerciseAuthorizationTest.t.sol @@ -10,6 +10,7 @@ import {PoolId} from "v4-core/types/PoolId.sol"; import {ExerciseRouter} from "../../src/ExerciseRouter.sol"; import {StandbyHook} from "../../src/StandbyHook.sol"; +import {ExerciseDeltaClosureRouter} from "../harness/ExerciseDeltaClosureRouter.sol"; import {BaseAuthenticBackingTest} from "./BaseAuthenticBackingTest.t.sol"; /*////////////////////////////////////////////////////////////// @@ -31,6 +32,15 @@ import {BaseAuthenticBackingTest} from "./BaseAuthenticBackingTest.t.sol"; /// proven about it is a property of the Hook's configuration rather than of a difference in /// 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. +/// /// 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, /// not the registry administrator, and not either router — so an authorization that succeeds cannot be @@ -51,9 +61,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: F8A neither reads nor forwards this field, so no test - /// outcome may depend on which value it is. The suites that are about the field say so explicitly - /// and vary it across the `uint256` domain. + /// 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. uint256 internal constant UNCONSTRAINED_MAX_INPUT = type(uint256).max; /*////////////////////////////////////////////////////////////// @@ -67,6 +78,8 @@ abstract contract BaseExerciseAuthorizationTest is BaseAuthenticBackingTest { unauthorizedExerciser = makeAddr("unauthorizedExerciser"); unconfiguredExerciseRouter = new ExerciseRouter(hook); + + _fundDeltaClosure(); } /*////////////////////////////////////////////////////////////// @@ -75,12 +88,36 @@ abstract contract BaseExerciseAuthorizationTest is BaseAuthenticBackingTest { /// @dev Activates the service with a real ExerciseRouter bound to this fixture's Hook. function _resolveExerciseRouter() internal virtual override returns (address router) { - configuredExerciseRouter = new ExerciseRouter(hook); + configuredExerciseRouter = new ExerciseDeltaClosureRouter(hook); router = address(configuredExerciseRouter); } - /// @dev Requests O2 authorization as an originating exerciser, through the configured ExerciseRouter. + /// @dev Gives the delta-closure router the currency it needs to close a committed execution's deltas. + /// + /// Not funding, in any economic sense: it is the balance the mechanical closure draws on so that + /// the real PoolManager will let an otherwise valid execution commit. No exerciser is funded, no + /// approval is granted, and nothing here decides who pays for an exercise — that is F8C's. + function _fundDeltaClosure() internal { + ustb.mint(exerciseRouter, ACTOR_FUNDING); + usdc.mint(exerciseRouter, ACTOR_FUNDING); + } + + /// @dev The ExerciseRouter a causal context is expected to be bound to. + /// + /// Overridable because a fixture may activate its service with a different coordinator — an + /// adversarial one, for the classification restrictions that must hold whatever the configured + /// router does. + function _expectedContextRouter() internal view virtual returns (address router) { + router = exerciseRouter; + } + + /// @dev Makes an O2 exercise request as an originating exerciser, through the configured ExerciseRouter. + /// + /// The request is the whole production coordination the router implements: it obtains Hook-owned + /// authorization and then performs the one protected execution that authorization admits. A + /// request that is refused at authorization therefore never reaches an execution, and one that + /// passes leaves execution evidence rather than a standing authorization. function _authorizeAs(address _exerciser, uint256 _commitmentId, uint256 _q) internal { _authorizeAs(_exerciser, _commitmentId, _q, UNCONSTRAINED_MAX_INPUT); } @@ -127,8 +164,12 @@ abstract contract BaseExerciseAuthorizationTest is BaseAuthenticBackingTest { assertEq(current.q, 0, _context); } - /// @dev Proves the Hook holds exactly the expected authorized causal bindings. - function _assertAuthorizationContext( + /// @dev Proves the Hook holds exactly the expected causal bindings, at an expected causal position. + /// + /// The bindings are the same object throughout the O2 operation: authorization writes them and no + /// later stage may substitute one. Only the position advances, so the position is the parameter. + function _assertCausalContext( + StandbyHook.ExerciseAuthorizationState _state, uint256 _commitmentId, address _exerciser, address _beneficiary, @@ -137,15 +178,47 @@ abstract contract BaseExerciseAuthorizationTest is BaseAuthenticBackingTest { ) internal view { StandbyHook.ExerciseAuthorizationContext memory current = _authorizationContext(); - assertEq(uint256(current.state), uint256(StandbyHook.ExerciseAuthorizationState.AUTHORIZED), _context); + assertEq(uint256(current.state), uint256(_state), _context); assertEq(PoolId.unwrap(current.serviceId), PoolId.unwrap(servicePoolId), _context); assertEq(current.commitmentId, _commitmentId, _context); - assertEq(current.exerciseRouter, address(configuredExerciseRouter), _context); + assertEq(current.exerciseRouter, _expectedContextRouter(), _context); assertEq(current.exerciser, _exerciser, _context); assertEq(current.beneficiary, _beneficiary, _context); assertEq(current.q, _q, _context); } + /// @dev Proves the Hook holds exactly the expected bindings, still awaiting its protected execution. + function _assertAuthorizationContext( + uint256 _commitmentId, + address _exerciser, + address _beneficiary, + uint256 _q, + string memory _context + ) internal view { + _assertCausalContext( + StandbyHook.ExerciseAuthorizationState.AUTHORIZED, _commitmentId, _exerciser, _beneficiary, _q, _context + ); + } + + /// @dev Proves a completed exercise request left exactly the expected bindings, with execution proven. + /// + /// This is what a request that passes every authorization predicate now reaches, because the + /// request coordinates the protected execution as well as the authorization. The bindings asserted + /// are the ones authorization resolved — the commitment, the authenticated exerciser, the + /// Beneficiary taken from the commitment record, and the quantity — so an authorization predicate + /// that resolved the wrong fact still fails here. + function _assertExercisedContext( + uint256 _commitmentId, + address _exerciser, + address _beneficiary, + uint256 _q, + string memory _context + ) internal view { + _assertCausalContext( + StandbyHook.ExerciseAuthorizationState.EXECUTED, _commitmentId, _exerciser, _beneficiary, _q, _context + ); + } + /// @dev The Supporting Capacity the canonical protected exercise of `q` would leave, from frozen /// geometry rather than from the derivation under test. /// diff --git a/test/unit/ExerciseAuthorization.t.sol b/test/unit/ExerciseAuthorization.t.sol index d408350..81574c3 100644 --- a/test/unit/ExerciseAuthorization.t.sol +++ b/test/unit/ExerciseAuthorization.t.sol @@ -101,7 +101,7 @@ contract ExerciseAuthorizationUnitTest is BaseUnbackedExerciseAuthorizationTest _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, EXERCISE_Q, "S' == O - q must be authorized" ); } @@ -220,7 +220,7 @@ contract ExerciseAuthorizationUnitTest is BaseUnbackedExerciseAuthorizationTest _authorizeAs(commitmentExerciseAuthority, commitmentId, EXERCISE_Q); - _assertAuthorizationContext( + _assertExercisedContext( commitmentId, commitmentExerciseAuthority, beneficiary, EXERCISE_Q, "the slot must still be usable" ); } diff --git a/test/unit/ProtectedExecutionEvidence.t.sol b/test/unit/ProtectedExecutionEvidence.t.sol new file mode 100644 index 0000000..3f26fd1 --- /dev/null +++ b/test/unit/ProtectedExecutionEvidence.t.sol @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/*////////////////////////////////////////////////////////////// + IMPORTS +//////////////////////////////////////////////////////////////*/ + +import {Test} from "forge-std/Test.sol"; + +import {HookMiner} from "v4-hooks-public/src/utils/HookMiner.sol"; + +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {TickMath} from "v4-core/libraries/TickMath.sol"; +import {BalanceDelta, toBalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolIdLibrary} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {SwapParams} from "v4-core/types/PoolOperation.sol"; + +import {DeployStandbyHook} from "../../script/DeployStandbyHook.s.sol"; +import {DeterministicFixtureDeployer} from "../../script/helpers/DeterministicFixtureDeployer.sol"; +import {StandbyFixtureConfig} from "../../script/helpers/StandbyFixtureConfig.sol"; + +import {StandbyHook} from "../../src/StandbyHook.sol"; +import {IEligibilityRegistry} from "../../src/interfaces/IEligibilityRegistry.sol"; +import {MockUSDC} from "../../src/mocks/MockUSDC.sol"; +import {MockUSTB} from "../../src/mocks/MockUSTB.sol"; + +import {StandbyHookHarness} from "../harness/StandbyHookHarness.sol"; +import {BaseUnbackedExerciseAuthorizationTest} from "../shared/BaseUnbackedExerciseAuthorizationTest.t.sol"; + +/*////////////////////////////////////////////////////////////// + CONTRACTS +//////////////////////////////////////////////////////////////*/ + +/// @notice Isolated evidence for the F8B causal transitions and for execution evidence the real +/// PoolManager cannot produce (G8B-5, G8B-6, G8B-8 through G8B-11). +/// @dev Two kinds of situation live here, and neither is reachable from production for the same reason: +/// the system works. +/// +/// The first is malformed execution evidence. Production evidence is the PoolManager's own +/// `BalanceDelta` for a swap it has just performed, so a delta on the wrong currency side, with the +/// wrong sign, or for an amount the swap did not produce cannot arise on any authentic path. Refusing +/// such evidence is nonetheless a requirement, and verifying that refusal means presenting evidence no +/// real swap would. +/// +/// The second is a causal position held open across a call. `EXECUTING` exists only between one +/// `beforeSwap` and its own `afterSwap`, inside a single PoolManager call frame nothing else can +/// enter, so the transitions guarding it can only be addressed one at a time from outside. +/// +/// The harness supplies exactly that: the ability to write a causal position and to hand the two +/// production mechanics facts of the test's choosing. It adds no check and removes none — the +/// predicates being exercised are the production ones, running against production causal state. What +/// is harness-created is the starting position, and nothing here may be read as evidence about +/// authorization, admission, enforcement, integration, or acceptance behavior. +contract ProtectedExecutionEvidenceTest 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 input side of a plausible protected execution delta. Its exact value is irrelevant: the + /// evidence comparison reads the protected-output side and nothing else. + int128 internal constant PLAUSIBLE_INPUT = -20_050_000_000; + + /*////////////////////////////////////////////////////////////// + G8B-5 — BEFORESWAP IS NOT EXECUTION + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves a matching proposal reaches the in-flight position and can reach nothing further. + /// @dev The whole distinction F8B rests on. The proposal is the exact admitted operation from the exact + /// bound router, and accepting it establishes only that the swap now in front of the PoolManager is + /// the authorized one. It is not evidence that any execution occurred, so the context advances to + /// `EXECUTING` and stops there. + function test_matchingProposal_reachesExecutingAndNotExecuted() public { + _seedContext(StandbyHook.ExerciseAuthorizationState.AUTHORIZED); + + serviceHarness.beginProtectedExecution(exerciseRouter, _protectedExactOutputSwapParams(SEEDED_Q)); + + assertEq( + uint256(_authorizationContext().state), + uint256(StandbyHook.ExerciseAuthorizationState.EXECUTING), + "an accepted proposal must reach EXECUTING" + ); + } + + /// @notice Proves no causal position other than AUTHORIZED admits a protected execution. + /// @dev The one restriction that covers a swap proposed before an authorization has decided, a second + /// swap while one is in flight, and another swap after execution. + function test_onlyAnAuthorizedContext_admitsAProtectedExecution() public { + StandbyHook.ExerciseAuthorizationState[3] memory refused = [ + StandbyHook.ExerciseAuthorizationState.AUTHORIZING, + StandbyHook.ExerciseAuthorizationState.EXECUTING, + StandbyHook.ExerciseAuthorizationState.EXECUTED + ]; + + for (uint256 i = 0; i < refused.length; ++i) { + _seedContext(refused[i]); + + vm.expectRevert( + abi.encodeWithSelector( + StandbyHook.StandbyHook__ExerciseExecutionNotAuthorized.selector, uint8(refused[i]) + ) + ); + serviceHarness.beginProtectedExecution(exerciseRouter, _protectedExactOutputSwapParams(SEEDED_Q)); + } + } + + /*////////////////////////////////////////////////////////////// + G8B-9, G8B-10 — EXACT ACTUAL OUTPUT + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves exactly the authorized protected output, and only that, establishes execution. + function test_exactProtectedOutput_establishesExecution() public { + _seedContext(StandbyHook.ExerciseAuthorizationState.EXECUTING); + + _recordEvidence(toBalanceDelta(PLAUSIBLE_INPUT, int128(int256(SEEDED_Q)))); + + assertEq( + uint256(_authorizationContext().state), + uint256(StandbyHook.ExerciseAuthorizationState.EXECUTED), + "exactly q on the protected-output side must establish execution" + ); + } + + /// @notice Proves every malformed shape of execution evidence is refused. + /// @dev Five different ways to be wrong, all failing the same exact comparison: one unit short, one unit + /// over, nothing at all, the right magnitude with the sign of a debt rather than a credit, and the + /// right magnitude on the input currency instead of the protected-output one. + function test_malformedExecutionEvidence_isRefused() public { + int128 q = int128(int256(SEEDED_Q)); + + _expectRefusedEvidence(toBalanceDelta(PLAUSIBLE_INPUT, q - 1), int256(SEEDED_Q) - 1); + _expectRefusedEvidence(toBalanceDelta(PLAUSIBLE_INPUT, q + 1), int256(SEEDED_Q) + 1); + _expectRefusedEvidence(toBalanceDelta(PLAUSIBLE_INPUT, int128(0)), int256(0)); + _expectRefusedEvidence(toBalanceDelta(PLAUSIBLE_INPUT, -q), -int256(SEEDED_Q)); + _expectRefusedEvidence(toBalanceDelta(q, PLAUSIBLE_INPUT), int256(PLAUSIBLE_INPUT)); + } + + /// @notice Proves execution evidence is refused unless an execution is actually in flight. + /// @dev `EMPTY -> EXECUTED` and `AUTHORIZED -> EXECUTED` are both refused, so intent can never become + /// evidence, and a second `EXECUTED` transition is refused so evidence can never be reused. + function test_executionEvidence_requiresAnExecutionInFlight() public { + StandbyHook.ExerciseAuthorizationState[3] memory refused = [ + StandbyHook.ExerciseAuthorizationState.EMPTY, + StandbyHook.ExerciseAuthorizationState.AUTHORIZED, + StandbyHook.ExerciseAuthorizationState.EXECUTED + ]; + + for (uint256 i = 0; i < refused.length; ++i) { + _seedContext(refused[i]); + + vm.expectRevert( + abi.encodeWithSelector( + StandbyHook.StandbyHook__NoProtectedExecutionInFlight.selector, uint8(refused[i]) + ) + ); + _recordEvidence(toBalanceDelta(PLAUSIBLE_INPUT, int128(int256(SEEDED_Q)))); + } + } + + /// @notice Proves evidence must belong to the exact accepted operation, not merely arrive during one. + /// @dev An in-flight execution does not make the next callback its own. Both conjuncts are falsified + /// separately: evidence presented by an account other than the bound ExerciseRouter, and evidence + /// claiming an operation other than the admitted one. + function test_evidenceForAnotherOperation_isRefused() public { + _seedContext(StandbyHook.ExerciseAuthorizationState.EXECUTING); + + BalanceDelta authentic = toBalanceDelta(PLAUSIBLE_INPUT, int128(int256(SEEDED_Q))); + + vm.expectRevert( + abi.encodeWithSelector( + StandbyHook.StandbyHook__NotAuthorizedExerciseExecutor.selector, address(swapPerimeter) + ) + ); + serviceHarness.recordProtectedExecution( + address(swapPerimeter), servicePoolKey, _protectedExactOutputSwapParams(SEEDED_Q), authentic + ); + + SwapParams memory otherOperation = _protectedExactOutputSwapParams(SEEDED_Q + 1); + + vm.expectRevert( + abi.encodeWithSelector( + StandbyHook.StandbyHook__NotTheAuthorizedProtectedExecution.selector, + otherOperation.zeroForOne, + otherOperation.amountSpecified, + otherOperation.sqrtPriceLimitX96 + ) + ); + serviceHarness.recordProtectedExecution(exerciseRouter, servicePoolKey, otherOperation, authentic); + + assertEq( + uint256(_authorizationContext().state), + uint256(StandbyHook.ExerciseAuthorizationState.EXECUTING), + "a refused presentation must not advance the context" + ); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @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 + }) + ); + } + + /// @dev Presents execution evidence for the admitted operation, from the bound router. + function _recordEvidence(BalanceDelta _delta) internal { + serviceHarness.recordProtectedExecution( + exerciseRouter, servicePoolKey, _protectedExactOutputSwapParams(SEEDED_Q), _delta + ); + } + + /// @dev Expects a presented delta to be refused as execution evidence, naming the output it carried. + function _expectRefusedEvidence(BalanceDelta _delta, int256 _reportedOutput) internal { + _seedContext(StandbyHook.ExerciseAuthorizationState.EXECUTING); + + vm.expectRevert( + abi.encodeWithSelector( + StandbyHook.StandbyHook__ProtectedOutputNotExecuted.selector, _reportedOutput, SEEDED_Q + ) + ); + _recordEvidence(_delta); + } +} + +/// @notice Isolated evidence that O2 classification and execution evidence follow the configured protected +/// direction (G8B-3, G8B-8). +/// @dev The canonical fixture is protected `zeroForOne`, so every other suite would pass unchanged if the +/// direction were hard-coded. This one activates a service whose protected direction is the mirror: +/// the protected output is currency0, the admitted execution is an exact-output swap in the +/// `oneForZero` direction, and the qualification boundary sits above the current price rather than +/// below it. +/// +/// No liquidity, swap, or commitment is needed to establish the point, because neither mechanic under +/// test consults any of them: the admitted operation is reconstructed from the immutable service +/// basis and the authorized quantity, and the evidence comparison reads one side of a delta. The +/// service is real, activated by the production transition against a real initialized pool; the causal +/// context is harness-written, and is evidence about these two mechanics alone. +contract ProtectedExecutionDirectionTest is Test { + using PoolIdLibrary for PoolKey; + + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @dev The protected execution-quality boundary of a `oneForZero` service: above the current price. + int24 internal constant MIRRORED_TICK_Q = 240; + + /// @dev The opposite realization-domain boundary of a `oneForZero` service. + int24 internal constant MIRRORED_TICK_O = -240; + + /// @dev The authorized quantity, in raw units of the protected output currency0. + uint256 internal constant SEEDED_Q = 20_000_000_000; + + address internal configurationAuthority; + address internal mirroredExerciseRouter; + + IPoolManager internal poolManager; + StandbyHookHarness internal serviceHarness; + + MockUSTB internal ustb; + MockUSDC internal usdc; + + PoolKey internal servicePoolKey; + + /*////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + + /// @notice Activates a real service whose protected direction is `oneForZero`. + function setUp() public { + configurationAuthority = makeAddr("mirroredConfigurationAuthority"); + mirroredExerciseRouter = makeAddr("mirroredExerciseRouter"); + + poolManager = IPoolManager(address(new PoolManager(address(this)))); + + DeployStandbyHook deployer = new DeployStandbyHook(); + + bytes memory constructorArgs = abi.encode( + poolManager, + configurationAuthority, + makeAddr("mirroredUniversalRouter"), + makeAddr("mirroredPositionManager") + ); + + (, bytes32 salt) = HookMiner.find( + address(this), + deployer.REQUIRED_HOOK_PERMISSION_MASK(), + type(StandbyHookHarness).creationCode, + constructorArgs + ); + + serviceHarness = new StandbyHookHarness{salt: salt}( + poolManager, + configurationAuthority, + makeAddr("mirroredUniversalRouter"), + makeAddr("mirroredPositionManager") + ); + + (ustb, usdc,) = new DeterministicFixtureDeployer().deployOrderedFixtureCurrencies(); + + servicePoolKey = PoolKey({ + currency0: Currency.wrap(address(ustb)), + currency1: Currency.wrap(address(usdc)), + fee: StandbyFixtureConfig.LP_FEE, + tickSpacing: StandbyFixtureConfig.TICK_SPACING, + hooks: IHooks(address(serviceHarness)) + }); + + poolManager.initialize(servicePoolKey, TickMath.getSqrtPriceAtTick(StandbyFixtureConfig.INITIAL_TICK)); + + vm.prank(configurationAuthority); + serviceHarness.configureAndActivate( + servicePoolKey, + false, + MIRRORED_TICK_Q, + MIRRORED_TICK_O, + IEligibilityRegistry(makeAddr("mirroredRegistry")), + mirroredExerciseRouter, + makeAddr("mirroredEstablishmentAuthority") + ); + } + + /*////////////////////////////////////////////////////////////// + MIRRORED PROTECTED DIRECTION + //////////////////////////////////////////////////////////////*/ + + /// @notice Proves the admitted operation is the mirrored one, and the canonical one is refused. + function test_mirroredService_admitsOnlyTheMirroredOperation() public { + _seedContext(StandbyHook.ExerciseAuthorizationState.AUTHORIZED); + + SwapParams memory canonicalDirection = SwapParams({ + zeroForOne: true, + amountSpecified: int256(SEEDED_Q), + sqrtPriceLimitX96: TickMath.getSqrtPriceAtTick(MIRRORED_TICK_Q) + }); + + vm.expectRevert( + abi.encodeWithSelector( + StandbyHook.StandbyHook__NotTheAuthorizedProtectedExecution.selector, + canonicalDirection.zeroForOne, + canonicalDirection.amountSpecified, + canonicalDirection.sqrtPriceLimitX96 + ) + ); + serviceHarness.beginProtectedExecution(mirroredExerciseRouter, canonicalDirection); + + serviceHarness.beginProtectedExecution(mirroredExerciseRouter, _mirroredOperation()); + + assertEq( + uint256(serviceHarness.exerciseAuthorization().state), + uint256(StandbyHook.ExerciseAuthorizationState.EXECUTING), + "the mirrored operation must be the admitted one" + ); + } + + /// @notice Proves the protected output is read from currency0 for a `oneForZero` service. + /// @dev The mirror of the canonical evidence rule, and the reason it cannot be hard-coded: exactly `q` + /// on currency1 — the input side here — is refused, and exactly `q` on currency0 establishes + /// execution. + function test_mirroredService_readsTheProtectedOutputFromCurrencyZero() public { + _seedContext(StandbyHook.ExerciseAuthorizationState.EXECUTING); + + int128 q = int128(int256(SEEDED_Q)); + + vm.expectRevert( + abi.encodeWithSelector( + StandbyHook.StandbyHook__ProtectedOutputNotExecuted.selector, int256(-20_050_000_000), SEEDED_Q + ) + ); + serviceHarness.recordProtectedExecution( + mirroredExerciseRouter, servicePoolKey, _mirroredOperation(), toBalanceDelta(-20_050_000_000, q) + ); + + serviceHarness.recordProtectedExecution( + mirroredExerciseRouter, servicePoolKey, _mirroredOperation(), toBalanceDelta(q, -20_050_000_000) + ); + + assertEq( + uint256(serviceHarness.exerciseAuthorization().state), + uint256(StandbyHook.ExerciseAuthorizationState.EXECUTED), + "currency0 must be the protected output of a oneForZero service" + ); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev The exact-output protected execution a `oneForZero` service admits. + function _mirroredOperation() internal pure returns (SwapParams memory params) { + params = SwapParams({ + zeroForOne: false, + amountSpecified: int256(SEEDED_Q), + sqrtPriceLimitX96: TickMath.getSqrtPriceAtTick(MIRRORED_TICK_Q) + }); + } + + /// @dev Writes a complete causal context at a chosen position. + function _seedContext(StandbyHook.ExerciseAuthorizationState _state) internal { + serviceHarness.writeExerciseAuthorization( + StandbyHook.ExerciseAuthorizationContext({ + state: _state, + serviceId: servicePoolKey.toId(), + commitmentId: 1, + exerciseRouter: mirroredExerciseRouter, + exerciser: makeAddr("mirroredExerciser"), + beneficiary: makeAddr("mirroredBeneficiary"), + q: SEEDED_Q + }) + ); + } +} From 24a2859f4853cbd9dba86ac8b0d894c1b88d49f1 Mon Sep 17 00:00:00 2001 From: garykocsis Date: Mon, 7 Sep 2026 23:56:57 -0400 Subject: [PATCH 2/2] session-12-retrospective --- .../session-12-chatgpt-record.md | 1071 +++++++++++++++++ 1 file changed, 1071 insertions(+) create mode 100644 docs/prompts/retrospective/session-12-chatgpt-record.md diff --git a/docs/prompts/retrospective/session-12-chatgpt-record.md b/docs/prompts/retrospective/session-12-chatgpt-record.md new file mode 100644 index 0000000..35ceea3 --- /dev/null +++ b/docs/prompts/retrospective/session-12-chatgpt-record.md @@ -0,0 +1,1071 @@ +# Session 12 --- ChatGPT Reasoning Record + +## F8B --- O2 Exact-Output Execution / Execution Evidence + +**Artifact type:** Non-normative curated reasoning record\ +**Session:** 12\ +**Final result:** F8B COMPLETE; G8B PASS / CLOSED\ +**Next slice:** F8C --- Authoritative Settlement / Direct Beneficiary +Delivery + +------------------------------------------------------------------------ + +## 1. Purpose + +This is the curated contemporaneous user ↔ ChatGPT reasoning record for +Session 12. It is evidence for the later methodology retrospective, not +a normative protocol artifact and not a substitute for +`docs/prompts/session-12-log.md`. + +It preserves the reasoning that materially affected F8B: responsibility +derivation, user challenges, design alternatives, G8B derivation, Claude +implementation scrutiny, independent source/test review, the bounded +correction cycle, and the final gate judgment. + +Where exact conversational wording is unavailable, entries are labeled +or presented as recovered decision records rather than invented +quotations. + +------------------------------------------------------------------------ + +## 2. Starting State --- What F8A Had Already Proven + +Session 12 began with F8A complete and G8A closed. + +F8A owned **authorization**, not execution. The Hook created one +transaction-scoped `AUTHORIZED` context only after proving: + +1. the request came through the configured ExerciseRouter/O2 + coordinator; +2. the real originating exerciser was recovered through the router's + authenticated transaction-local mechanism rather than trusted + forwarded calldata; +3. the commitment existed and belonged to the configured service; +4. the originating exerciser equaled the commitment exercise authority; +5. the commitment remained valid; +6. `exercisableFrom <= now < validUntil`; +7. the Beneficiary remained eligible; +8. `0 < q <= Remaining`; +9. the prospective protected exact-output execution yielded `S′`; +10. the Hook derived current authoritative `O`; +11. `S′ >= O - q`. + +Equality was admissible. + +F8A did not reduce Remaining or O and did not claim execution, +settlement, delivery, fulfillment, or finalization. + +The F8B blocker was therefore narrower: + +> Given one already-authorized exercise, what authoritative evidence is +> sufficient to prove that the exact protected AMM execution admitted by +> that authorization actually occurred? + +------------------------------------------------------------------------ + +## 3. Exact F8B Responsibility + +The key distinction was **authorization truth versus execution truth**. + +The Hook-owned `AUTHORIZED` context can prove that an exact exercise +**may occur**. It cannot prove that the AMM execution **did occur**. + +The ExerciseRouter cannot become the authority for execution merely +because it coordinates the call. A router success flag, return value, +forwarded assertion, or self-report would let the coordinator +manufacture Standby execution truth. + +The Uniswap v4 PoolManager is authoritative over the actual AMM +transition. The Hook remains authoritative over the Standby +interpretation of that transition. + +The responsibility was frozen as: + +> **F8B authoritatively binds one existing Hook-owned AUTHORIZED +> exercise to exactly one qualifying protected PoolManager swap, +> classifies only that exact swap as O2, and transitions the causal +> context from AUTHORIZED to EXECUTED only after authoritative +> PoolManager callback evidence proves that the matching swap actually +> produced exactly the authorized protected output q.** + +This decomposed into: + +1. **O2 Execution Classification** +2. **Authoritative Execution Evidence** + +The compact authority rule was: + +> **PoolManager owns execution truth; Hook owns Standby causal +> interpretation; ExerciseRouter owns neither.** + +------------------------------------------------------------------------ + +## 4. Deriving the Exact Qualifying O2 Swap + +For authorized quantity `q`, the attempted protected operation had to +satisfy: + +``` text +callback external msg.sender == immutable PoolManager +AND callback sender == configured ExerciseRouter +AND PoolId == configured/authorized service PoolId +AND params.zeroForOne == configured protected direction +AND params.amountSpecified > 0 +AND params.amountSpecified == q +AND params.sqrtPriceLimitX96 == authoritative qualification limit +AND context phase == AUTHORIZED +``` + +Positive `amountSpecified` is exact-output in the pinned v4 +implementation. Thus the sign is part of the execution mode, not merely +a numeric check. + +The qualification price limit had to reuse the authoritative service +qualification boundary already used by F5/F8A prospective +protected-exercise derivation. F8B was not allowed to independently +restate that economic boundary. + +This produced a Single Normative Ownership requirement: + +> The protected execution used for prospective authorization and the +> protected execution accepted during real execution must be one +> authoritative reconstruction rather than parallel descriptions that +> can drift. + +The canonical fixture remained MockUSTB/MockUSDC, `zeroForOne`, while +production had to generalize to `oneForZero`. + +------------------------------------------------------------------------ + +## 5. O2 Versus O3 --- Causal Exclusion + +A major question was whether ordinary O3 activity could continue after +authorization but before the protected swap. + +That was rejected. Allowing an unrelated swap could mutate pool state +after F8A had authorized against a specific prospective state. + +The derived classifier was: + +``` text +context == EMPTY: + ordinary O3 classification/enforcement + +context == AUTHORIZED: + exact authorized O2 swap -> O2 + anything else -> REVERT + +context == EXECUTING: + any additional beforeSwap -> REVERT + +context == EXECUTED: + any additional beforeSwap -> REVERT +``` + +An active authorization therefore creates an **O2 causal exclusion +zone**. + +Two important consequences followed: + +- unrelated O3 cannot mutate state between authorization and protected + execution; +- a malformed O2 attempt cannot fail O2 classification and silently + fall through to O3. + +This was treated as causal continuity, not merely as a reentrancy guard. + +------------------------------------------------------------------------ + +## 6. Why `EXECUTING` Was Necessary --- and More State Was Not + +The economic causal model was conceptually: + +``` text +EMPTY -> AUTHORIZED -> EXECUTED -> later consumed +``` + +But PoolManager exposes distinct `beforeSwap` and `afterSwap` callbacks. + +Moving directly to `EXECUTED` in `beforeSwap` would treat acceptance of +a proposal as proof of execution. Therefore the minimum implementation +guard was: + +``` text +AUTHORIZED -> EXECUTING -> EXECUTED +``` + +Meaning: + +- `AUTHORIZED`: the exact operation may begin; +- `EXECUTING`: the exact proposal passed authenticated `beforeSwap` + classification and is in flight; +- `EXECUTED`: authenticated `afterSwap` evidence proves exact + protected output. + +Rejected transitions included: + +``` text +EMPTY -> EXECUTING/EXECUTED +AUTHORIZED -> EXECUTED from beforeSwap alone +AUTHORIZED -> ordinary O3 +EXECUTING -> second beforeSwap +EXECUTING -> EXECUTED without q evidence +EXECUTED -> second execution +overwrite of EXECUTED authorization +``` + +A nonce, execution hash, persistent execution ID, or swap ledger was +considered unnecessary unless implementation revealed a genuine +ambiguity. Authenticated PoolManager sequencing plus exact callback +facts and Hook-owned context already provided causal binding. + +This was a direct Semantic Minimality decision. + +------------------------------------------------------------------------ + +## 7. Requested Output Is Not Actual Execution Evidence + +A central distinction was: + +``` text +params.amountSpecified == q +``` + +versus: + +``` text +actual protected-output delta == +q +``` + +The first proves only that `q` was **requested**. + +Pinned Uniswap v4 behavior permits an exact-output swap to stop at the +price limit before producing the full requested output. That may be a +valid Uniswap execution but cannot count as a successful Standby +protected exercise. + +Therefore `afterSwap` evidence was mandatory. + +For `zeroForOne`: + +``` text +delta.amount1() == +q +``` + +For `oneForZero`: + +``` text +delta.amount0() == +q +``` + +The sign is meaningful. No absolute-value shortcut is valid. + +The rule became: + +> **Exact-output request proves the attempted operation; exact signed +> PoolManager output delta proves successful protected execution.** + +Partial output `< q` must reject. + +------------------------------------------------------------------------ + +## 8. Partial Exact-Output and Economic Atomicity + +If Uniswap reaches the qualification boundary before producing `q`, +Standby must not create a partial-execution economic state. + +The correct sequence is: + +1. `beforeSwap`: `AUTHORIZED -> EXECUTING`; +2. PoolManager executes; +3. `afterSwap` observes actual output; +4. output other than exactly `q` causes revert; +5. the PoolManager transition and transient causal write unwind + atomically. + +No `PARTIALLY_EXECUTED` state was introduced because the economic +agreement did not assign meaning to one. + +This was an application of Economic Atomicity: + +> A protected exercise either establishes the complete execution fact +> required by the agreement or establishes no execution fact at all. + +------------------------------------------------------------------------ + +## 9. `maxInput` and the F8B/F8C Boundary + +`maxInput` already existed in the F8A request/context surface after the +Session 11 correction. + +That correction had established: + +> **Implementation discretion may fill genuinely open realization +> details, but it may not silently remove, reinterpret, or relocate a +> responsibility or surface already assigned by the frozen handoff.** + +Once F8B executes the exact-output swap, PoolManager produces both +protected output and an input-side debt. + +F8B needs only the protected-output side as execution evidence. + +F8C owns: + +``` text +authoritative actual input debt +actualInput <= maxInput +settlement of that debt +direct Beneficiary delivery +``` + +F8B therefore deliberately avoided storing `actualInput` merely because +BalanceDelta made it observable. + +The boundary was: + +> F8B may observe BalanceDelta but consumes only the protected-output +> component as execution evidence. The actual-input component becomes +> economically relevant in F8C. + +This distinction later exposed the only issue found in independent +review: stale F8A-era explanatory comments. + +------------------------------------------------------------------------ + +## 10. The Production Completion Problem + +A real v4 swap opens PoolManager currency deltas. F8B could execute the +swap, but closing those deltas belongs to F8C settlement/delivery. + +Therefore a production F8B transaction could not yet complete the full +unlock lifecycle. + +The solution was **not** to leak F8C into F8B. + +Instead, real-PoolManager tests were allowed narrowly scoped **test-only +mechanical delta closure** so the swap could commit and its post-state +could be inspected. + +The test seam was prohibited from: + +- becoming production semantics; +- assigning an authoritative payer; +- enforcing `maxInput`; +- claiming exerciser payment; +- delivering to the Beneficiary; +- reducing Remaining or O; +- claiming fulfillment. + +The resulting principle was: + +> **The proof obligation is canonical; the concrete proof technique is +> downstream.** + +------------------------------------------------------------------------ + +## 11. Failed Execution and Catch/Retry + +If `afterSwap` rejects evidence, the `EXECUTING` write is inside the +reverted PoolManager call subtree and rolls back. + +That raised a question: could the router catch the failure and try +another swap while the outer authorization survived? + +The canonical answer was no. + +The production ExerciseRouter must propagate protected-execution failure +and must not catch-and-retry. Then failure unwinds: + +``` text +afterSwap failure +-> PoolManager swap revert +-> unlock revert +-> ExerciseRouter exercise revert +-> F8A authorization creation revert +``` + +No surviving authorization remains available for a second attempt. + +Because catch/retry was prohibited in the canonical router, F8B did not +add a pre-PoolManager `beginExecution` surface, retry counter, or +persistent attempt nonce. + +G8B had to verify the actual production router obeyed this assumption. + +------------------------------------------------------------------------ + +## 12. G8B Derived Before Implementation + +The expanded verification gate covered: + +1. authorization prerequisite; +2. exact configured coordinator; +3. exact service identity; +4. exact protected direction and mirrored `oneForZero`; +5. exact-output mode; +6. exact requested `q`, including mismatch rejection; +7. exact authoritative qualification boundary; +8. O2 causal exclusion; +9. `beforeSwap` reaches only `EXECUTING`; +10. nested/multiple isolation; +11. PoolManager execution authority; +12. exact signed actual output `+q`; +13. partial-output rejection; +14. causal correspondence without unnecessary nonce/hash state; +15. one transition to `EXECUTED`; +16. failure atomicity and router propagation; +17. F5 prediction versus actual committed PoolManager state; +18. Remaining and O unchanged; +19. no F8C leakage; +20. no F8D leakage; +21. O3 regression; +22. prior-gate regression; +23. unit, fuzz, adversarial, transition, real-PoolManager integration, + and differential evidence, with GI deferred. + +The gate tested responsibility and causal truth, not merely whether a +swap occurred. + +------------------------------------------------------------------------ + +## 13. Claude Implementation --- Material Choices + +Claude implemented F8B and maintained `docs/prompts/session-12-log.md`. + +### Production ExerciseRouter acquired R3 + +Claude mapped the router decomposition as: + +``` text +R1 + R2 -> F8A +R3 -> F8B +R4 -> F8C +R5 -> F8D +``` + +R3 is exactly one protected exact-output PoolManager execution. + +The production ExerciseRouter therefore unlocks PoolManager and issues +one protected swap after authorization. + +This became a major independent-review item: coordination could move to +the router, but Standby economic truth could not. + +The implementation preserved that boundary by asking the Hook for the +canonical currently authorized operation instead of composing direction, +quantity, or qualification boundary independently. + +### One canonical reconstruction + +Claude extracted `_canonicalProtectedExecution(q)` and reused it for: + +- F8A prospective derivation; +- the router proposal; +- `beforeSwap` classification; +- `afterSwap` revalidation. + +This was a strong Single Normative Ownership result. + +### `authorizedProtectedExecution()` read surface + +The router obtains the current admitted operation through a Hook view. + +Independent review treated this as acceptable because it grants no +authority: the Hook still revalidates everything on the authenticated +PoolManager callback path. + +### Minimal causal state + +Claude added `EXECUTING`/`EXECUTED` but no nonce, hash, persistent +execution record, or economic snapshot. + +### Test-only delta closure + +`ExerciseDeltaClosureRouter` follows the production execution path and +then mechanically closes deltas from its own pre-funded balances. +Protected output is parked on the closure contract, intentionally +preventing tests from confusing mechanical closure with Beneficiary +delivery. + +------------------------------------------------------------------------ + +## 14. Independent Review Inputs + +ChatGPT did not accept Claude's proposed PASS as the gate decision. + +The user uploaded the changed production, harness, shared-test, +integration, unit, fuzz, and perimeter files. ChatGPT reconciled the +uploads against Claude's changed-file list and identified two initially +missing files: + +``` text +test/unit/ExerciseAuthorization.t.sol +test/fuzz/ExerciseBackingFuzz.t.sol +``` + +The user supplied both before the final review. + +This ensured the gate review covered the actual reported F8B change +surface rather than a representative subset. + +------------------------------------------------------------------------ + +## 15. Independent Review --- Production Boundary + +The production implementation passed substantive review. + +### Hook-owned classification + +With `EMPTY`, `_beforeSwap` follows existing O3 behavior. + +With active O2 context, the operation must pass protected-execution +classification. Only `AUTHORIZED` can begin execution. `AUTHORIZING`, +`EXECUTING`, and `EXECUTED` cannot fall through to O3. + +### Exact operation + +The canonical operation contains: + +- configured direction; +- positive exact-output amount; +- exact `q`; +- exact qualification limit. + +### Router identity is necessary but insufficient + +The callback sender must be the configured ExerciseRouter, but router +identity alone cannot manufacture O2. Adversarial tests show an +O2-shaped swap from the configured router without Hook authorization +remains subject to the ordinary perimeter. + +### `beforeSwap` is not execution proof + +The accepted proposal reaches only `EXECUTING`. + +### `afterSwap` establishes execution evidence + +`afterSwap` rechecks causal facts, selects the direction-correct +BalanceDelta side, and requires exact positive `q` before `EXECUTED`. + +No absolute-value bug or request-as-evidence shortcut was found. + +------------------------------------------------------------------------ + +## 16. Independent Review --- Failure Atomicity + +The production ExerciseRouter was inspected for: + +- `try/catch`; +- retry logic; +- alternate success branches; +- second PoolManager execution; +- router-authored success flags. + +None were found. + +It performs one protected execution and propagates failure. Therefore an +evidence failure unwinds the entire exercise transaction, including +authorization. + +The minimal causal model was sufficient. + +------------------------------------------------------------------------ + +## 17. Independent Review --- Delta-Closure Harness + +The test-only closure seam passed the responsibility-leakage review. + +It does not: + +- enforce `maxInput`; +- define the exerciser as payer; +- establish an authoritative payer; +- deliver output to the Beneficiary; +- reduce Remaining; +- reduce O; +- record fulfillment; +- finalize the commitment. + +Output is deliberately parked on the closure contract. + +This was important because a convenient test router could otherwise have +silently pre-implemented F8C merely to make F8B integration tests +complete. + +------------------------------------------------------------------------ + +## 18. Independent Review --- Prediction Differential + +F8B did not rely solely on observing `EXECUTED`. + +The integration evidence captures the F5/F8A prospective Supporting +Capacity, executes through the real PoolManager, then compares the +actual production post-state and independent reference reconstruction +against the prediction. + +The fuzz suite repeats the relationship across admissible quantities. + +This was strong evidence that authorization-time economic derivation and +real AMM execution converge on the same state. + +------------------------------------------------------------------------ + +## 19. Independent Review --- Direction Generality + +Because the canonical fixture is `zeroForOne`, direction hard-coding +could have escaped ordinary tests. + +The mirrored `oneForZero` test verified: + +- the admitted canonical exact-output operation uses the mirrored + direction and appropriate qualification boundary; +- protected output is read from currency0 instead of currency1. + +This was accepted as sufficient F8B direction-generality evidence for +the direction-dependent mechanics. + +------------------------------------------------------------------------ + +## 20. Reachability Qualifications + +Two notable qualifications were reviewed and accepted. + +### Nested `EXECUTING -> beforeSwap` + +A genuine second `beforeSwap` between the accepted swap's `beforeSwap` +and `afterSwap` is structurally unreachable through the real PoolManager +call frame. The production predicate is therefore tested directly at +unit level. + +This was accepted because the missing integration path follows from +PoolManager sequencing rather than missing test effort. + +### Partial output from authentic backed state + +Authentically backed authorization should prevent a protected execution +from reaching the qualification boundary before producing `q`. + +To test F8B's independent defense, a harness writes an +otherwise-unreachable unbacked remainder and then executes through the +real PoolManager. Partial output is produced and `afterSwap` rejects it. + +This was accepted as defense-in-depth evidence: upstream state should +make the condition unreachable, but F8B still refuses invalid evidence +if forced. + +------------------------------------------------------------------------ + +## 21. F8C/F8D Leakage Review + +No production implementation was found for: + +``` text +actualInput <= maxInput +input debt settlement +direct Beneficiary delivery +Remaining reduction +O reduction +fulfillment +commitment completion +final causal-context consumption +``` + +`EXECUTED` therefore means only: + +> the exact authorized AMM execution occurred and produced exactly the +> authorized protected output. + +It does **not** mean that the exercise is settled, delivered, fulfilled, +or finalized. + +------------------------------------------------------------------------ + +## 22. Defect Found --- Documentation Drift + +The substantive implementation passed, but ChatGPT did not immediately +close G8B. + +Several F8A-era comments still explained `maxInput` by saying, in +effect: + +> no swap executes at this slice, so there is no input debt yet. + +That had been true in F8A but became false after F8B implemented R3. + +The correct statement is: + +> `maxInput` remains inert through F8B not because no swap occurs, but +> because enforcement of the actual input debt belongs to F8C together +> with settlement. + +This was classified as: + +- not a production-semantic defect; +- not a reason to redesign F8B; +- a documentation-only gate hold. + +The interim judgment was: + +``` text +F8B implementation semantics: PASS +G8B behavioral/evidence obligations: PASS +Responsibility-leakage gate: PASS +Semantic-minimality gate: PASS +Economic-atomicity gate: PASS +Single-normative-ownership gate: PASS + +G8B: HOLD for one documentation-only correction +``` + +------------------------------------------------------------------------ + +## 23. Bounded Correction + +The follow-up prompt was: + +``` text +docs/prompts/session-12-f8b-documentation-correction.md +``` + +It prohibited production behavior changes, test behavior changes, +assertion changes, storage changes, fixture changes, F8C/F8D +implementation, GI, and project-status changes. + +Claude corrected four stale statements: + +1. `src/ExerciseRouter.sol` --- `exercise(...)` NatSpec; +2. `test/fuzz/ExerciseAuthorizationFuzz.t.sol` --- + `testFuzz_maxInput_changesNoAuthorizationOutcome`; +3. `test/periphery/ExerciseAuthorizationPerimeter.t.sol` --- + `test_maxInputOfZero_authorizesIdentically`; +4. `test/shared/BaseExerciseAuthorizationTest.t.sol` --- + `UNCONSTRAINED_MAX_INPUT`. + +The `_authorizeAs` helper description was also updated to reflect the +full production coordination path. + +Importantly, similar-looking statements were deliberately retained when +still accurate: + +- `StandbyHook.authorizeExercise` itself executes no swap; +- authorization remains distinct from exercise; +- no production exercise **completes** in F8B because settlement is + absent; +- fulfillment still does not exist. + +This selective correction was preferable to mechanical +search-and-replace because it respected the subject and scope of each +statement. + +------------------------------------------------------------------------ + +## 24. Verification and Final Gate + +After the correction Claude reported: + +``` text +forge fmt --check clean +forge lint no findings +forge build successful +forge test 466 passed, 0 failed, 0 skipped +FOUNDRY_PROFILE=ci forge test 466 passed, 0 failed, 0 skipped +``` + +No production logic, test logic, assertion, interface, storage layout, +or fixture behavior changed. + +The final independent decision was: + +> **F8B --- O2 Exact-Output Execution / Execution Evidence: COMPLETE** + +> **G8B --- PASS / CLOSED** + +The reachability qualifications did not prevent closure because they +reflect genuine properties of the staged architecture and PoolManager +call structure. + +------------------------------------------------------------------------ + +## 25. Status Synchronization + +Only after independent G8B closure was Claude authorized to update +`docs/project-status.md`. + +The status-only instruction was: + +``` text +F8B — O2 Exact-Output Execution / Execution Evidence: COMPLETE +G8B: PASS +F8C — Authoritative Settlement / Direct Beneficiary Delivery: + next authorized implementation slice / current blocker +``` + +The prompt prohibited implementation details, design commentary, test +summaries, gate reasoning, and retrospective observations in the status +artifact. + +This preserved artifact ownership: + +> `project-status.md` records project state; session logs and +> retrospectives preserve the evidence and reasoning that established +> it. + +------------------------------------------------------------------------ + +## 26. Material User Challenges and Process Decisions + +### Recovered decision record --- preserve the Working Model + +The user had previously challenged handoffs that omitted the explicit +Working Model. That concern governed Session 12: + +- ChatGPT derives responsibility and gate before implementation; +- Claude implements within the frozen boundary; +- ChatGPT independently reviews actual source/tests; +- Claude's proposed PASS is evidence, not the gate decision. + +### Recovered decision record --- complete review inputs + +When the user asked whether any uploaded files were missing, ChatGPT +reconciled them against Claude's recorded change set rather than +reviewing an incomplete sample. The two missing tests were requested and +supplied. + +### Recovered decision record --- bounded correction + +When only stale comments remained, ChatGPT held G8B for a +documentation-only correction instead of ignoring the inconsistency or +reopening architecture. The user accepted the bounded correction path. + +### Recovered decision record --- status discipline + +After closure, the user requested the same constrained status-update +pattern used in prior sessions. The resulting prompt authorized state +synchronization only. + +------------------------------------------------------------------------ + +## 27. Rejected Alternatives + +### Router-authored execution truth + +Rejected because coordination does not confer authority over the +economic meaning of execution. + +### `beforeSwap` as execution proof + +Rejected because acceptance of a proposal is not evidence of its result. + +### `amountSpecified == q` as sufficient proof + +Rejected because v4 exact-output may stop at the price limit with output +`< q`. + +### Absolute-value delta checks + +Rejected because signed delta direction is authoritative evidence. + +### Ordinary O3 during `AUTHORIZED` + +Rejected because it can mutate pool state between authorization and +protected execution. + +### Malformed O2 falling through to O3 + +Rejected because an active authorization creates an exclusive causal +window. + +### Persistent nonce/hash/ledger + +Rejected under Semantic Minimality because callback sequencing and +Hook-owned context already resolve correspondence. + +### Catch-and-retry + +Rejected because failure propagation gives cleaner atomicity and +prevents repeated attempts without extra state. + +### Early F8C settlement for test convenience + +Rejected because verification convenience does not justify +responsibility leakage. + +### Storing `actualInput` in F8B + +Rejected because F8B does not own its economic use. + +### Mechanical replacement of every "no swap" comment + +Rejected because some statements remained correctly scoped to +authorization or completion. + +------------------------------------------------------------------------ + +## 28. Methodology Observations + +### 28.1 Authority follows the fact being proven + +F8B cleanly separates three roles: + +- the router coordinates; +- PoolManager establishes the AMM execution fact; +- the Hook interprets that fact under Standby semantics. + +Candidate retrospective formulation: + +> **Execution Authority Separation = Coordinator ≠ Executor ≠ Protocol +> Interpreter** + +This is preserved as a methodology observation, not frozen here as a new +principle. + +### 28.2 Requested behavior is not observed behavior + +`q` in the call parameters expresses intent. The signed PoolManager +delta expresses outcome. + +General observation: + +> Admission/request facts should not substitute for authoritative +> post-transition evidence when the underlying mechanism may legally +> produce a different result. + +### 28.3 Verification plumbing must not acquire economic meaning + +The delta-closure harness made real execution provable without changing +the production responsibility boundary. + +Candidate formulation: + +> **Verification Mechanism ≠ Protocol Mechanism** + +Again, this is an observation for post-project evaluation rather than a +frozen principle. + +### 28.4 Stable responsibility does not imply stable implementation-era rationale + +The `maxInput` owner never changed, but the old explanation for why it +was downstream became false once F8B began executing the swap. + +Observation: + +> **Stable responsibility does not imply stable implementation-era +> rationale.** + +Staged implementation reviews should therefore inspect explanatory text +for stale reasons even when normative ownership is unchanged. + +### 28.5 Continuity with the Session 11 `maxInput` lesson + +Session 11 established that implementation discretion cannot silently +remove or relocate an assigned surface. + +Session 12 added the complementary lesson: as upstream behavior +advances, explanatory text must evolve without moving the responsibility +itself. + +Together: + +> A frozen responsibility boundary must survive both implementation +> convenience and implementation progression. + +### 28.6 Implementation Convergence evidence + +Before Claude implemented F8B, the session had already fixed: + +- authorization ownership; +- execution-truth ownership; +- exact qualifying swap; +- causal states; +- exact evidence; +- partial-output behavior; +- retry policy; +- F8C boundary; +- verification obligations. + +Claude's remaining discretion was largely realization-level: helper +extraction, the Hook read surface, R3 router coordination, and test-only +closure. + +The independent review could therefore evaluate implementation against +pre-existing responsibilities rather than retroactively inventing the +design standard. This is direct evidence for the previously frozen +Implementation Convergence Principle. + +------------------------------------------------------------------------ + +## 29. Final Session State and Handoff + +At Session 12 completion: + +``` text +F0 COMPLETE / G0 PASS +F1 COMPLETE / G1 PASS +F2 COMPLETE / G2 PASS +F3 COMPLETE / G3 PASS +F4 COMPLETE / G4 PASS +F5 COMPLETE / G5 PASS +F6A COMPLETE / G6A PASS +F7 COMPLETE / G7 PASS +F6B COMPLETE / G6B PASS +F8A COMPLETE / G8A PASS +F8B COMPLETE / G8B PASS +``` + +The next authorized slice is: + +> **F8C --- Authoritative Settlement / Direct Beneficiary Delivery** + +F8C inherits the F8B fact that an exact authorized protected PoolManager +execution occurred and produced exactly `q` protected output. + +F8C must not re-decide F8A authorization or F8B execution truth. Its +blocker is to: + +- identify the authoritative actual input debt produced by that exact + execution; +- enforce carried `maxInput` against that debt; +- settle the PoolManager debt; +- deliver exactly `q` protected output directly to the authoritative + Beneficiary; +- preserve the later F8D fulfillment/finalization boundary. + +No F8C implementation was authorized during Session 12. + +------------------------------------------------------------------------ + +## 30. Retrospective Verdict + +Session 12 preserved a difficult staged integration boundary without +collapsing execution, settlement, delivery, and fulfillment into one +implementation step. + +The causal proof established by F8B is: + +``` text +Hook-owned authorization + -> +exact PoolManager proposal + -> +authenticated beforeSwap classification + -> +in-flight causal guard + -> +authoritative PoolManager execution + -> +authenticated afterSwap result + -> +exact signed protected-output evidence + -> +Hook-owned EXECUTED interpretation +``` + +while deliberately refusing to claim settlement, Beneficiary delivery, +fulfillment, or finalization. + +The independent review found no substantive F8B defect. The only +correction required was explanatory documentation whose F8A-era +rationale became stale after R3 was implemented. The correction changed +no behavior, and G8B then closed. + +**Final Session 12 result: F8B COMPLETE; G8B PASS / CLOSED.**