Skip to content

ALP State Mutation Pipeline#16

Open
jordanschalm wants to merge 17 commits intomainfrom
jord/alp-state-mutations
Open

ALP State Mutation Pipeline#16
jordanschalm wants to merge 17 commits intomainfrom
jord/alp-state-mutations

Conversation

@jordanschalm
Copy link
Copy Markdown
Member

@jordanschalm jordanschalm commented Apr 23, 2026

This PR adds a proposal for how to applying state mutations and enforcing invariants in FlowALP. The ideas should be communicated in the PR diff, so I'll keep this description short :)

jordanschalm and others added 9 commits April 23, 2026 11:31
Structure operations as validate → apply → check invariants. Validators
construct resource-typed mutations (access(contract) init); Pool.applyMutations
is the sole state-write choke point, gated by a new MutateState entitlement.
Vault inputs are modeled as mutations (VaultDeposit); vault outputs remain
direct effects bounded by the invariant check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the validator / StateMutation pipeline with a simpler orchestrator
shape: Pool API methods build plain-struct Intents (DepositIntent,
WithdrawIntent), validators are `view` yes/no gates over &Pool, and mutators
are contract-level functions that take auth(MutateState) &Pool + the intent
+ any input resources. Mutators apply all state changes, call
checkInvariants, and return any output resources directly.

Eliminates the StateMutation resource interface and the resource-in-resource
VaultDeposit pattern. Resource I/O is explicit at the mutator signature:
inputs alongside the intent, outputs as return value.

Access convention: Pool methods are either `view` or entitlement-gated.
MutateState gates the appliers (applyLedgerDelta, applyVaultDeposit,
applyReserveWithdraw) and checkInvariants. The entitlement is minted only
inside orchestrator methods and never escapes the operation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move all mutable protocol state (positions, tokenStates, reserves) into a
new PoolState resource nested inside Pool. Pool becomes a thin orchestrator
owning config, lifecycle, access control, and @PoolState. Pool has no
state-writing methods of its own — it builds intents and forwards to
PoolState's entry points.

Rename the applier-gating entitlement MutateState → StateWrite (it now
belongs to PoolState, not Pool). Validators and mutators operate on
&PoolState / auth(StateWrite) &PoolState. PoolState exposes two layers:
access(all) entry points (deposit, withdraw, registerPosition, registerToken)
that run validate → apply → checkInvariants internally, and access(StateWrite)
appliers reached only through mutator functions.

Also tightens Reserves' mutating methods (deposit/withdraw/addSupportedToken)
from access(contract) to access(StateWrite), mirroring PoolState's convention.

The design consolidates state-writing code into one greppable place. It does
NOT prevent Pool from reaching PoolState appliers via direct self.state.X
access — Cadence allows a composite unrestricted access to its own nested
resources, so enforcement remains discipline + convention + lint. The top-
level doc block calls this caveat out explicitly.

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

Move applyDeposit and applyWithdraw from contract-level functions onto
PoolState as access(self) methods. Tighten all state-writing methods on
PoolState (applyLedgerDelta, applyVaultDeposit, applyReserveWithdraw,
applyDeposit, applyWithdraw, checkInvariants) from access(StateWrite) to
access(self). The StateWrite entitlement is no longer used anywhere and is
removed.

Unlike entitlement gating — which Cadence bypasses for direct self.field.method()
calls within an owning composite — access(self) is enforced by the compiler
even across composite boundaries. Verified by probe: an access(all) method on
Pool calling self.state.applyLedgerDelta fails to compile with "access denied:
function requires `self` authorization". Pool genuinely cannot reach PoolState's
state-writing methods; the only path is through PoolState's access(all) entry
points, which run validation first.

Also reverts Reserves' mutating methods (deposit, withdraw, addSupportedToken)
from access(StateWrite) to access(contract), since StateWrite is gone and
Reserves is only called from PoolState's access(self) appliers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Describes the three-component pipeline (Orchestrator → Validator → Mutator)
and the two design goals driving it: minimum state-writing surface area
and uniform invariant enforcement after every operation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the Mutation Pipeline design with a pre-validation phase for
time-dependent state (interest indices, utilization, etc.). Each intent
Orchestrator now calls state.applyTimeBasedMutations() before invoking
its Mutator so validation runs against time-advanced state.

Also adds explicit guidance on check placement: what belongs in the
Validator (per-request / operation-mode / cheap early-exit checks) vs
what belongs in Invariants (universal post-state properties).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jordanschalm jordanschalm changed the title Explore invariant-based software design ALP State Mutation Pipeline Apr 24, 2026
Compress PoolState's deposit/applyDeposit and withdraw/applyWithdraw pairs
into single access(all) Mutators (applyDeposit, applyWithdraw). Each Mutator
expresses its Validator as a pre condition (calling FlowALP.validateXxx,
which now returns Bool) and the universal invariant check as a post condition
(calling self.invariantsHold(), also Bool-returning).

Apply the same pre/post pattern to registerPosition. Adds a matching
validateRegisterPosition view function for consistency.

Validators (validateDeposit, validateWithdraw, validateRegisterPosition)
remain separate contract-level view functions — restructured to return Bool
so they can be invoked from pre blocks. Vault cross-checks (type, balance
match the intent) stay inline in applyDeposit's pre because they need the
resource reference. PoolState gains an isSupportedToken view to expose
supported-token lookup to external validators.

Renames checkInvariants to invariantsHold (returns Bool) so it can be
used as a post condition.
Update the design doc to reflect that Validators now return Bool (called
from Mutator pre blocks) and that the universal post-state check is
invariantsHold (called from Mutator post blocks). Replaces the old
"validator panics; checkInvariants runs as the final phase" narrative
with the actual pre/body/post structure.

Updates: Summary pipeline diagram and bullets, Goal 2 phrasing, Validator
section signature and semantics, Mutator section code example and phase
descriptions, Invariants section, Access-control convention list.
Remove DepositIntent and WithdrawIntent — for simple operations the struct
is redundant with the resource (vault carries tokenType and amount) or with
the underlying parameters. Mutator signatures now take primitive arguments
and resources directly. A multi-input op like liquidation may reintroduce
a parameter-bundle struct when the operation lands.

Remove validateDeposit / validateWithdraw / validateRegisterPosition. The
validation rules now live inline as Cadence pre-conditions on each Mutator,
which is more idiomatic, gives granular per-rule error messages, and removes
the parallel Bool-returning function layer that existed only to be called
from pre.

Net result: Pool's orchestrators are pure pass-throughs; PoolState's
Mutators are self-contained pre/body/post units. The full pipeline is
Orchestrator -> applyTimeBasedMutations -> Mutator (pre, body, post).

Update the design doc to match: removes the Intent subsection, rewrites
the Validator subsection to describe pre-conditions as the Validator,
updates Mutator examples and the access-control convention.
Now that there are no contract-level applyDeposit/applyWithdraw functions
to disambiguate against, the shorter names read better. Pool's orchestrators
forward as self.state.deposit(...) / self.state.withdraw(...).

Updates the contract code, top-of-file design block, and design doc.
@jordanschalm jordanschalm marked this pull request as ready for review April 24, 2026 22:54
@jordanschalm jordanschalm requested a review from a team as a code owner April 24, 2026 22:54
Base automatically changed from jord/alp-skeleton to main April 24, 2026 23:53
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.

1 participant