Skip to content

Add claim upkeep - #20

Merged
stefanobotticelli merged 12 commits into
mainfrom
add_claim_upkeep
Sep 24, 2026
Merged

stefanobotticelli merged 12 commits into
mainfrom
add_claim_upkeep

Conversation

@kalrashivam

Copy link
Copy Markdown
Collaborator

Auto-claim via a dedicated keeper

Adds keeper-driven claim settlement on top of fix_withdraw_request's crystallization work.
Until now, claimEpochAssets/batchClaimEpochAssets were the only way to get paid, and both
require msg.sender == claim.user — a user (or someone holding their key) had to submit their
own transaction. This PR adds an automatic path without removing that one.

What changed

EpochedQueueModule.keeperSettleClaims(epochId, claimIds) — new function, pays each claim
directly to claim.user regardless of who calls it. Self-claim is untouched and remains the
permissionless fallback if no keeper is run, or one lags; a claim settled by either path is a
no-op for the other (idempotent on EpochClaim.claimed).

Deliberately atomic — not try/catch-per-claim internally. Swallowing a failed transfer inside the
loop would let claim.claimed get set without the money actually moving, which is a fund-loss
bug, not a liveness nicety. If any claim in a batch fails, the whole batch reverts; the automation
layer (below) is where liveness/retry belongs.

src/automation/ClaimSettlementUpkeep.sol — new, separate Chainlink-Automation-compatible
contract. Deliberately not an added Op on the existing VaultUpkeep:

VaultUpkeep.checkUpkeep is a strict single-op-per-tick priority chain (EPOCH_FUND > EPOCH_CLOSE > RECONCILE > CRYSTALLIZE > REBALANCE > STRATEGY_REBALANCE > DEPLOY > REALIZE), each
tier already cooldown/starvation-tuned against the others. Claim settlement is a different shape
of work — not one vault-wide state flip but a per-user backlog needing its own pagination cursor
across many ticks. Slotting it into that chain would either starve it (low priority, rarely
reached on an active vault) or destabilize the existing tuning (high priority). A separate
contract gets its own Chainlink registration and cadence instead, and doesn't touch VaultUpkeep
at all.

  • Scans forward from a persistent (epochId, claimId) cursor, bounded by owner-configurable
    maxClaimsPerUpkeep (default 20) / maxScanPerUpkeep (default 200, so a long run of
    already-claimed IDs can't blow the gas budget).
  • performUpkeep failure (try/catch — the same liveness pattern VaultUpkeep already uses)
    leaves the cursor untouched; it retries the same batch next tick.
  • Owner escape hatch, excludeClaim(epochId, claimId, bool): a claim whose owner can't receive
    the asset would otherwise block its whole batch forever, atomically, on every retry. Excluding
    it only affects this keeper's scan — the user can still self-claim if their address later works.

Test infra fix: test/helpers/CoreHarness.sol wires module selectors from a hand-maintained
list, not from SelectorLib — it was also missing rollCapEpochIfNeeded (added in the parent
branch), so no test had exercised that as a standalone external call until now. Both gaps closed.

Selectors

QUEUE_MODULE_SELECTORS: 10 → 11 (+keeperSettleClaims).

Tests

12 new (test/unit/automation/ClaimSettlementUpkeep.t.sol):

  • payout goes to claim.user, never the keeper caller
  • batching across multiple ticks with a bounded batch size
  • cursor correctly skips a still-unfunded epoch to find work in a later funded one
  • idempotency against self-claim, in both orders (keeper-first and self-claim-first)
  • a failed performUpkeep leaves the cursor in place and retries
  • the exclusion escape hatch

Full suite: 991/991 passing. EpochedQueueModule 17,521/24,576 bytes (EIP-170 margin: 7,055).

Out of scope / follow-up

  • Not deployed or registered with Chainlink Automation.
  • rollCapEpochIfNeeded (from the parent branch) still isn't wired into any keeper — separate
    open decision, not addressed here.

shivam kalra and others added 5 commits September 22, 2026 03:01
…uest

Implements "Withdrawal Model — Economic Exit at Request". Once a standard or
instant withdrawal request is accepted, the user is a creditor with a fixed
claim. The epoch no longer prices anything; it is only a settlement bucket.

Core changes
- _crystallizeExit: single shared helper for standard + instant requests.
  Order: solvency check -> NAV validity gate -> fee split -> assetsOwed =
  convertToAssets(net) (rounded down) -> fee shares to FeeCollector -> burn
  net shares -> totalOwed += assetsOwed. Instant fallback stores the same
  assetsOwed; never re-prices or re-applies a fee.
- Accounting primitives on CoreVault (single source of truth): grossAssets(),
  totalOwed(), liabilityIndex() (pure, unstored), isInsolvent(), liabilityState().
  totalAssets() = max(0, gross - totalOwed), saturating. reservedForClaims only
  earmarks liquidity and is not subtracted from NAV.
- Removed: cancelEpochWithdrawal, ppsAtClose, escrowedShares, closedPendingAssets,
  totalEscrowedShares, withdrawal minimum (ClaimTooSmall).
- Claims pay assetsOwed x liabilityIndex; each claim releases only its own
  epoch's earmark.
- Insolvency (gross < owed): deposits/mints/new requests revert; close, fund and
  claims continue. fundEpoch funds at nominal x index (not the impossible nominal)
  so claims pay the same recovery ratio in any order. Events:
  InsolvencyEntered/Exited, syncInsolvencyState().
- Insolvency funding tolerates <=10 bps shortfall (insolvency only): found on the
  Arbitrum fork, real lending adapters cannot return the last wei of a position.
- NAV validity gate (CoreVault.navStatus, StrategyRouter.navValidity): a request
  is rejected unless the warm cache is complete and <=15 min old (age tested
  explicitly), every enabled strategy's valuation is readable and OK in the health
  registry, and any configured oracle is live/fresh/consistent. No soft refresh.
- Class-A consumers (BufferManager, StrategyRouter, LiquidityOps, VaultUpkeep,
  lens) now read grossAssets(); checkOracleFreshness body moved to an internal
  function to keep StrategyRouter under EIP-170.
- Zero-equity (gross == owed) blocks deposits/mints/requests like insolvency;
  forceWithdrawAll returns 0 without burning when totalAssets() == 0.

Deviations from the spec (need sign-off, see docs/economic-exit.md section 4)
- _crystallizeExit takes an extra ExitMode parameter for the fee tier.
- An instant request that falls back to the queue pays the instant fee tier.
- Instant cap is measured on the net assetsOwed.
- Funding at nominal x index instead of nominal (spec 7.2) — the spec's nominal
  target strands the last epoch in insolvency.

Tests
- EconomicExit_Spec (W-1..W-15 + section 11 scenarios), EconomicExit_Invariants
  (stateful), AdapterHack_Scenarios (adapter-loss timing cases, 30-vs-50 owed),
  EconomicExit_ArbitrumFork (17 tests + real Fluid adapter hack, real oracle,
  health registry, warm adapters).
- ~25 existing test files migrated to the new model; cancel/minClaimAmount
  tests replaced. MockBufferManagerForTests gains an always-fresh mode.

Docs: docs/economic-exit.md (consumer audit, deviations, deployment plan for a new
CoreVault, live-system findings). Older docs carry a "superseded" banner.

Requires a NEW CoreVault deployment (totalAssets lives in the non-proxied shell).
Live finding: BufferManager.navRefreshInterval is 6h vs the 15-min gate; keeper
cadence (I-10) must be fixed before go-live.
EpochedQueueModule.fundEpoch(): stop stranding a solvent epoch on adapter
withdrawal slippage. It previously asked strategies for exactly the deficit,
so each retry shrank the residue by the same factor and stalled a unit or two
short (a 1-unit ask can return 0). It now asks for deficit + 0.5% buffer + 1
(STRATEGY_REDEEM_BUFFER_BPS), floored at MIN_STRATEGY_REDEEM (10,000 units)
so a tiny ask's rounding can't look like a loss-cap breach and revert the
whole redeem. Beyond the router's own loss cap, funding still refuses
atomically and stays Closed until governance raises the cap or uses
emergencyRedeemBatch — unchanged, documented behavior, not papered over.

New tests, run against the rebased fix_withdraw_request branch:

- AdapterShortfall.t.sol (7 tests): a real StrategyRouter + a strategy that
  pays a fixed % short (not a hack — slippage/rounding). Covers: funds in one
  call within the buffer; retries converge to the last unit beyond it; beyond
  the loss cap the router refuses atomically and unblocks via cap raise or
  emergency redeem; insolvent + short-paying together still converges to the
  right recovery ratio; two claimants both paid nominal while solvent.

- EconomicExit_Gaps.t.sol (12 tests): the NAV gate closing during a hack
  doesn't trap users (force exit still pays; accepted claims still settle);
  fuzzed claim pro-rata across any loss size and claim order (256 runs);
  the top-up-on-recovery path reverts InsufficientFreeLiquidity rather than
  paying short, then pays full once liquidity returns; perf-fee
  crystallization is a clean no-op in insolvency; deposits work again after
  recovery at the recovered price; 25 dust claims in deep insolvency drain
  totalOwed/reservedForClaims/outstandingClaimCount to exactly zero; two
  successive hacks across funded and unfunded epochs settle correctly in
  mixed claim order; four adversarial cases (donation-before-exit is a loss
  for the donor, front-run of a known loss pays the fee, 150 dust requests
  don't block real claims, a top-up race never pays a later claimant short).

- EconomicExit_ArbitrumFork.t.sol (+1 test): a real strategy whose
  withdraw()/withdrawAll() start reverting (paused/hacked market) makes
  fundEpoch() fail safe — epoch stays Closed, nothing earmarked, claim
  reverts EpochNotFunded — and recovers on its own once the strategy works
  again, realising through the real router.

Full suite incl. Arbitrum fork tests: 1002/1003 passing. The one failure,
test_Isolation_Euler, is pre-existing and unrelated (live Euler market
headroom at the fork's "latest" block).
…ng gaps

Second review round (Stefano + Pier) on the economic-exit branch. Fixes
everything raised except one open product decision (documented, not
implemented — see below).

EpochedQueueModule
- _crystallizeExit: soft-refresh the warm NAV before the strict freshness
  gate (same pattern as deposit/mint), instead of only ever reverting on a
  cache nobody happened to poke recently. Every existing stale-NAV test now
  forces the refresh itself to fail, so the fix can't make them unreachable.
- _crystallizeExit: revert ZeroAmount when assetsOwed rounds to 0. Not a
  withdrawal minimum -- stops a worthless claim from occupying a slot in
  outstandingClaimCount, the dynamic-cap queue-depth signal.
- requestInstantWithdrawal: decide the fee tier BEFORE crystallizing, via a
  conservative pre-fee gross estimate against cap and liquidity, then
  crystallize exactly once. A fallback into the queue now settles at the
  standard fee, never the instant one.
- requestInstantWithdrawal / requestEpochWithdrawal: deposit lock is now a
  hard revert (DepositLockActive) on both paths, checked before any
  crystallization. Previously only the instant path consulted it, and only
  as a soft "fall back to the queue" signal -- since a request crystallizes
  regardless of path, that silently let a locked user's shares be burned
  during the lock window. Force exit remains the deliberate bypass.
- requestInstantWithdrawal: instant cap base is now snapshotted at
  cap-epoch rollover (CoreStorage.capBaseSnapshot) instead of read live, so
  standard-queue activity (which shrinks totalAssets() via totalOwed)
  cannot shrink the supposedly-independent instant bucket mid-epoch.
- _canInstant: liquidity waterfall is hot -> one warm refill attempt -> queue
  fallback, never a strategy redeem, matching the buffer architecture.
- fundEpoch: now tops up an already-Funded epoch when liabilityIndex has
  recovered past what's earmarked, via the same pull waterfall as a
  first-time fund. Previously fundEpoch on a Funded epoch was an
  unconditional no-op, so a recovered claim could revert
  InsufficientFreeLiquidity forever with no permissionless way to add the
  extra cash in.
- MIN_STRATEGY_REDEEM replaced with _minStrategyRedeem(), derived from the
  asset's decimals instead of hardcoded to 6.

CoreStorage / CoreVault / lens
- Single MAX_WARM_NAV_AGE constant (CoreStorage), sourced by CoreVault,
  ERC4626Module and EpochedQueueModule instead of three separate literals.
- New capBaseSnapshot field + CoreVault.capBaseSnapshot() getter.
- CoreVaultLens.calculateCapImmediateRemaining reads the same snapshot
  (falling back to live totalAssets() only if nothing has been snapshotted
  yet) so the preview matches what's actually enforced.

Params / config
- IParamsProvider.WithdrawalParams.minClaimAmount and
  GlobalConfig.WithdrawalConfig.minClaimAmount marked @dev DEPRECATED
  (unused for exits under spec §6.4) rather than removed -- 16 deploy/ops
  files, including a dedicated SetMinClaimAmount.s.sol, read or assert on
  it; removal is a follow-up PR that touches that tooling.

Tests
- New: W-12 fallback-pays-standard-fee proof, dust-tolerance exact-drift
  test, five-claimant insolvency pro-rata, real-router + stale-oracle +
  scarce-hot independence test, live deposit-lock fork test.
- Rewrote every test that used lockPeriod as a trick to force the
  instant-fallback branch (four of them) -- that trick no longer works now
  that lock hard-reverts; switched to cap exhaustion or a drained hot
  balance instead.
- Rewrote the stale-NAV tests (unit + fork) to force the refresh itself to
  fail, for the same reason.
- Fork test helper: deposits now clear the live 1-day deposit lock (warp +
  re-refresh NAV) so existing scenarios aren't accidentally testing lock
  behaviour; added a dedicated lock test using the real, unmodified state.
- EpochedQueueModule's internal size-gate target raised 16KB -> 20KB
  (measured 16,915B; ~7.6KB of margin below the real EIP-170 limit) --
  the module's scope grew well past "small, stateless" this round.

Docs (docs/economic-exit.md §8)
- Full writeup of every fix above, plus the one item NOT fixed: Pier's
  creditor-parity point (a claimant paid during a dip is worse off than one
  who waits out a later recovery, even though W-14 still holds at any
  single instant). Traced and confirmed real; two possible resolutions
  scoped (cohort-crystallized recovery vs. per-claim residual entitlement)
  but not decided here -- flagged for Multyr.
- §8.3: 5 fork tests + the pre-existing, unrelated test_Isolation_Euler
  fail only on the free public RPC's archive-state limits when the real
  UsdcMultiLendingVault strategy rebalances across its own sub-adapters
  mid-withdrawal -- traced with -vvvv, confirmed outside src/ entirely.

Full suite: 1002/1008 passing; all 6 failures are the RPC limitation above.
…e remaining instant-cap coupling

Resolves the two items from PR #19's second-round review reply (Option A for
creditor parity, and the cap-coupling follow-up).

Option A — crystallized recovery ratio per insolvency cohort
--------------------------------------------------------------
- EpochData gains `recoveryIndex` (WAD), set exactly once when fundEpoch()
  first transitions a cohort Closed -> Funded, after its realize waterfall
  (hot -> warm -> strategy) has run: reserve/realize first, crystallize
  second, not at the first block grossAssets < totalOwed is observed.
- The haircut, if any, is written out of totalOwed in the same instant
  (EpochRecoveryCrystallized), so an already-settled cohort stops inflating
  totalOwed/NAV/isInsolvent() for as long as it happens to stay unclaimed.
- claimEpochAssets/batchClaimEpochAssets pay assetsOwed * epoch.recoveryIndex,
  never the live cross-epoch liabilityIndex(). recoveryIndex is immutable:
  fundEpoch() on an already-Funded epoch is a pure no-op again (the top-up-
  on-recovery path from the prior round is removed — it directly
  contradicted finality). A recovery after crystallization flows to
  remaining shareholders instead of the settled cohort.

Cap coupling, fully closed
---------------------------
- _epochCapRemaining()/CoreVaultLens no longer read outstandingClaimCount at
  all: standard queue depth now has zero effect on the instant bucket
  (an enabled DynamicCapParams just pins the cap at maxBps).
- New permissionless rollCapEpochIfNeeded() lets a keeper snapshot
  capBaseSnapshot right at the cap-epoch boundary, instead of it being
  captured lazily by whichever instant withdrawal happens to arrive first
  (which standard-queue activity could already have shrunk).

Audit fixes
-----------
- CoreVaultLens: fixed a lens/module dynamic-cap sentinel mismatch (0 vs
  type(uint16).max for "unlimited") that could make the lens report zero
  instant capacity while the contract itself allowed unlimited withdrawals.
- Formally deprecated DynamicCapParams/DynamicCapConfig (@dev DEAD /
  DEPRECATED, same treatment as minClaimAmount): its only remaining signal
  is gone, so an enabled config no longer scales anything toward minBps.

Selectors: +rollCapEpochIfNeeded (QUEUE_MODULE_SELECTORS 9 -> 10).
Tests updated for the new per-cohort semantics; full non-fork suite green
(979/979). docs/economic-exit.md §8 updated to record both resolutions.
…tion contract

Enables auto-claim without requiring every user to submit their own
transaction, while leaving self-claim as the permissionless fallback.

EpochedQueueModule
-------------------
- New keeperSettleClaims(epochId, claimIds): pays each claim directly to
  claim.user regardless of caller (unlike claimEpochAssets/
  batchClaimEpochAssets, which require msg.sender == claim.user). Atomic per
  batch, not try/catch-per-claim internally -- swallowing a failed transfer
  there would let claim.claimed be set without the money moving, a fund-loss
  bug. A claim settled by either this or the existing self-claim path is a
  no-op for the other (idempotent on EpochClaim.claimed).
- New selector wired (QUEUE_MODULE_SELECTORS 10 -> 11).

Automation
----------
- New src/automation/ClaimSettlementUpkeep.sol: a SEPARATE Chainlink-
  Automation-compatible contract, deliberately not an added Op on
  VaultUpkeep. VaultUpkeep's checkUpkeep is a strict single-op-per-tick
  priority chain already tuned across 8 competing ops; claim settlement is a
  different shape of work (a per-user backlog needing its own pagination
  cursor across many ticks, not a single vault-wide state flip) that would
  either starve at the bottom of that chain or destabilize it at the top.
- Scans forward from a persistent (epochId, claimId) cursor, bounded by
  owner-configurable maxClaimsPerUpkeep/maxScanPerUpkeep. A failed
  performUpkeep (try/catch, same liveness pattern VaultUpkeep already uses)
  leaves the cursor untouched and retries next tick.
- Owner escape hatch (excludeClaim) for a claim whose owner can't receive
  the asset -- otherwise blocks its whole batch, atomically, on every retry.

Test infra fix
---------------
- test/helpers/CoreHarness.sol wires module selectors from a hand-maintained
  list, not from SelectorLib -- it was also missing rollCapEpochIfNeeded
  (added earlier this branch), so no test had exercised that as a
  standalone external call until now. Both gaps closed.

12 new tests (test/unit/automation/ClaimSettlementUpkeep.t.sol), incl. the
core property (payout goes to claim.user, not the keeper caller), batching
across ticks, cursor skipping an unfunded epoch, idempotency against
self-claim in both orders, and the exclusion escape hatch. Full suite:
991/991. EpochedQueueModule 17,521/24,576 bytes (EIP-170).

Not deployed/registered with Chainlink Automation -- that, and whether to
also wire rollCapEpochIfNeeded into an existing keeper, are separate
deploy-time decisions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kalrashivam
kalrashivam changed the base branch from main to fix_withdraw_request September 23, 2026 12:04
shivam kalra added 2 commits September 24, 2026 16:48
…t spec doc

Moves cap-epoch bookkeeping into rollCapEpochIfNeeded (ExitEngineLib), which
snapshots totalAssets() into capBaseSnapshot on rollover so the withdrawal
cap base is fixed at epoch start rather than recomputed against a moving
NAV. Retires DynamicCapConfig's stress-throttle fields to plain
compatibility storage (GlobalConfig) now that capPerEpochBps is the only
supported static cap knob.

Rewrites docs/economic-exit.md from PR-description/deviations-log form into
a standing reference doc for withdrawal accounting, valuation gates and
settlement, closing out the previously open spec decisions. Updates
architecture/audit-scope/fee-policy/governance/modules/recovery docs and
the unit/integration/fork/invariant suites to match.
- Size unfunded cohorts from free assets and require valid NAV for haircuts
- Snapshot caps before mutations, refresh instant pricing, and ignore dynamic caps
- Restrict funding shortfalls to one base unit
- Prevent cursor manipulation and revisit skipped epochs
- Isolate failed claims, back off retries, and respect claim pauses
- Handle keeper-settled FeeCollector claims
- Add regression coverage and align documentation with current behavior
@kalrashivam8375

Copy link
Copy Markdown

Addressed the funding and claim-settlement review findings in 8e64fc4.

Funding and cap handling

  • N1: Unfunded cohorts use assets and liabilities net of reservedForClaims, so the second cohort can fund before the first claims.
  • N2: Haircut crystallization requires valid navStatus(), including after liquidity reconciliation. Invalid attempts emit EpochFundingNavInvalid and leave the epoch Closed.
  • A2: Standard/instant requests, deposit/mint, force exits and funding roll the cap epoch before changing assets or supply. Zero epoch duration safely skips rollover.
  • B2: Instant requests refresh warm NAV before checking eligibility and pricing.
  • Dynamic cap parameters are ignored by enforcement and the lens; capPerEpochBps controls the cap.
  • Funding/claim shortfall tolerance is limited to one underlying base unit. A new regression verifies that a two-unit illiquid remainder leaves the epoch Closed without writing down liabilities.

Claim automation and FeeCollector

  • performUpkeep recomputes the scan from on-chain state and ignores caller-supplied cursor data.
  • A bounded circular scan revisits skipped unfunded epochs and retryable claims.
  • Failed batches fall back to individual claim calls so healthy claims can settle. Failed claims receive a one-hour retry delay; unsuccessful/maintenance runs have a one-minute cooldown.
  • checkUpkeep returns false while funded claims are paused; performUpkeep also checks the pause.
  • harvestQueued recognizes keeper-settled claims, removes their pending entries and distributes available underlying. It also clears settled entries when the underlying has already been distributed.

Documentation and validation

  • Aligned economic-exit.md with request-time pricing, immutable cohort recovery and current cap/keeper behavior. Removed the superseded banners from audit-scope.md, fee-policy.md and modules.md, and cleaned reviewer attributions and stale historical wording in comments/docs.
  • Local core/automation run: 655 passed, 0 failed. Final economic-exit regression run, including the new two-unit boundary test: 21 passed, 0 failed. These runs overlap; fork tests were not run.

These fixes are currently on this PR's branch. The requested CoreHarness change split into #19 has not been performed; PR history has not been rewritten.

shivam kalra and others added 4 commits September 24, 2026 17:52
…aircuts

- Add fundedOutstandingClaimCount to EpochedQueueModule (wired through
  IQueueModule, SelectorLib/SelectorRegistry, CoreHarness) so
  ClaimSettlementUpkeep can skip checkUpkeep/performUpkeep entirely when
  there's nothing funded to settle, instead of burning LINK on empty scans.
- Have the settlement scan skip Funded epochs that are already fully
  claimed, so the keeper cursor doesn't stall re-scanning drained epochs.
- fundEpoch() now attempts a soft warm-NAV refresh before crystallizing a
  shortfall, so a stale warm NAV can't lock in an incorrect recovery ratio;
  a failed refresh leaves the epoch uncrystallized instead.
- Add claim-settlement-operations.md and insolvency-runbook.md; update
  architecture, queue-mechanics, exit-engine, storage-layout,
  access-control, and economic-exit docs to match.
- Extend ClaimSettlementUpkeep and EconomicExit_Gaps test suites to cover
  the new counter and NAV-refresh paths.
Persist bounded scan progress and sleep until funded claims change,
a retry expires, or owner controls reset the scan.
Add excluded-claim, backoff, wake-up and wraparound regression tests,
and update settlement operations docs.
An idle scan compared only fundedOutstandingClaimCount. When an epoch funds
and a claim settles in the same block, that count returns to the recorded
value and the newly funded claims stay unswept.

Add fundedEpochCount to EpochQueueStorage.Layout, appended after
fundedOutstandingClaimCount so no existing slot moves. fundEpoch increments
it on each Closed -> Funded transition and it never decreases. Expose it as
a public view and wire the selector.

ClaimSettlementUpkeep records both counters in its scan pass and wakes or
invalidates the pass when either one changes.
@stefanobotticelli
stefanobotticelli changed the base branch from fix_withdraw_request to main September 24, 2026 18:55
main carries PR #19 as squash commit 3415af7, whose tree is identical to
88f5d5b, the PR #19 head this branch already contains. Every one of the 14
conflicted files resolves to this branch's version: the main side equals
88f5d5b and the branch side adds only PR #20 changes.

Four files merged without a conflict marker are restored to this branch's
version as well (docs/access-control.md, docs/architecture.md,
docs/fee-policy.md, docs/storage-layout.md): the three-way merge against
the older merge base re-added document banners that PR #20 removes.

The resulting tree is identical to the pre-merge branch tree.
@stefanobotticelli
stefanobotticelli merged commit 9a9d120 into main Sep 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants