Purpose: Context document for follow-up sessions. Summarizes the current architecture, identifies issues, and proposes improvements.
FlowALP is a lending/borrowing protocol on Flow. The core contracts are:
| Contract | LOC (approx) | Responsibility |
|---|---|---|
FlowALPv0 |
~2170 | Pool resource: deposits, withdrawals, liquidations, rebalancing, governance, fee collection, health computation |
FlowALPModels |
~2300 | Data models, interfaces, and implementations for positions, pool state, pool config, token state, balance types, entitlements |
FlowALPHealth |
~330 | Health math: adjusted balances after deposits/withdrawals, required deposits for target health |
FlowALPInterestRates |
~140 | Interest curves: FixedCurve, KinkCurve |
FlowALPPositionResources |
~510 | User-facing Position resource, PositionManager, PositionSink/PositionSource DeFiActions connectors |
FlowALPEvents |
~320 | Centralized event definitions and access(account) emission functions |
FlowALPRebalancerv1 |
~340 | Self-custody scheduled rebalancer |
FlowALPRebalancerPaidv1 |
~180 | Managed (paid) rebalancer service |
FlowALPSupervisorv1 |
~66 | Cron-based supervisor for paid rebalancers |
PriceOracleAggregatorv1 |
~410 | Multi-source oracle aggregation with history stability |
PriceOracleRouterv1 |
~82 | Per-token oracle routing |
MOET |
~215 | FungibleToken stablecoin (mean-of-exchange token) |
User → Position resource (in user account)
→ Capability<EPosition> → Pool resource (in protocol account)
→ PoolState (reserves, token states, position queue, insurance/stability funds)
→ InternalPosition (per-position balances, queued deposits, sink/source)
→ PoolConfig (oracle, risk factors, DEX, pause state)
- Health Factor:
H = effectiveCollateral / effectiveDebt ≥ 1.0(liquidation threshold) - Reserve solvency:
reserve[T].balance ≥ Σ(credit balances for T) - Σ(debit balances for T)— reserves back net deposits - Interest index monotonicity: credit/debit indices only increase
- Reentrancy: position lock prevents concurrent operations on the same position
FlowALPv0.Pool is a ~1200-line resource (within a ~2170-line contract) that handles:
- Position lifecycle (create, deposit, withdraw)
- Liquidation logic (manual liquidation with DEX price comparison)
- Rebalancing (under/overcollateralized positions, sink/source interaction)
- Interest rate management (time-based updates, compounding)
- Insurance fee collection (reserve withdrawal → swap to MOET)
- Stability fee collection (reserve withdrawal → stability fund)
- Governance operations (pause, unpause, add tokens, set rates, set curves, set oracle)
- Health computation (balance sheet construction, health factor)
- Deposit rate limiting
- Async position updates
Impact: Auditing any single concern requires reading and understanding the entire resource. A bug in fee collection could interact with reserve withdrawals in unexpected ways.
PoolState holds reserves, insurance fund, stability funds, position queues, position locks, token states, default token, and the position ID counter. Everything is gated behind a single EImplementation entitlement.
Impact: Any code path with EImplementation can withdraw from reserves, modify token state, deposit to the insurance fund, etc. There's no structural enforcement that, say, insurance collection only withdraws the calculated insurance amount.
Pool reserves (@{Type: {FungibleToken.Vault}}) are directly borrowable by any internal method via state.borrowReserve(type). The same reference that the deposit path uses to add funds is the one that liquidation, fee collection, and withdrawals all use to remove funds.
Impact: It's difficult to verify that reserves are only withdrawn through sanctioned paths. An auditor must trace every call site of borrowReserve and borrowOrCreateReserve to confirm correctness.
InternalBalance.recordDeposit() and recordWithdrawal() take an auth(EImplementation) &{TokenState} reference and directly call increaseCreditBalance, decreaseDebitBalance, etc. This means a per-position struct has side effects on global accounting.
Impact: The coupling makes it hard to reason about what changes global state. A reader must understand that calling position.borrowBalance(type)!.recordDeposit(...) not only changes the position's balance but also the pool's total credit/debit balances, which in turn triggers interest rate recalculation.
The code itself notes (line 73 of FlowALPv0): "this logic partly duplicates FlowALPModels.BalanceSheet construction in _getUpdatedBalanceSheet". The maxWithdraw function at the contract level and _getUpdatedBalanceSheet inside Pool both iterate over position balances to compute effective collateral/debt, but use different code paths.
Similarly, TokenSnapshot creation is duplicated across buildTokenSnapshot, buildPositionView, availableBalance, and the contract-level maxWithdraw.
Insurance and stability fee collection (_collectInsurance, _collectStability, updateInterestRatesAndCollectInsurance, updateInterestRatesAndCollectStability) are methods on the Pool that directly withdraw from reserves and interact with external DEX swappers.
Impact: Fee collection failure modes (insufficient reserves, DEX price deviation) can potentially affect core lending operations if called in the wrong sequence.
Collateral factors and borrow factors live in PoolConfig (as {Type: UFix64} maps), while everything else about a token (interest curves, deposit limits, insurance rates) lives in TokenState. This split means building a TokenSnapshot requires reading from both PoolConfig and TokenState.
EImplementation gates everything from "set position lock" to "withdraw from reserves" to "modify interest curves". There's no way to grant partial internal access.
The core idea: replace the monolithic Pool with a coordinator that delegates to specialized modules, each with its own resource interface, invariants, and access control.
┌─────────────────┐
│ Pool (thin │ ← coordinates modules; no direct state
│ coordinator) │
└──────┬──────────┘
┌────────────┬───┴────┬─────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
┌────────────┐ ┌────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐
│ Reserve │ │ Token │ │ Position │ │ Fee │ │Liquidation│
│ Vault Mgr │ │ Ledger │ │ Registry │ │ Collector │ │ Engine │
└────────────┘ └────────┘ └──────────┘ └───────────┘ └──────────┘
Responsibility: Sole custodian of FungibleToken vaults. Provides typed withdrawal functions that make the intent and authorization explicit.
Key invariant: Every vault withdrawal is through a purpose-specific function, making it trivially auditable.
Interface: See cadence/contracts/proposals/ReserveVaultManager.cdc
Responsibility: Per-token accounting — interest indices, total credit/debit balances, interest curve, deposit capacity. Replaces the TokenState portion of PoolState.
Key change: recordDeposit/recordWithdrawal return an EffectsDelta struct instead of directly mutating global state. The caller (Pool coordinator) applies effects explicitly.
Interface: See cadence/contracts/proposals/TokenLedger.cdc
Responsibility: Per-position balance tracking, health parameters, queued deposits, position locks. Replaces the positions dictionary and position-related fields of PoolState.
Key change: Position balance mutations return descriptive effect objects rather than having side effects on global state.
Interface: See cadence/contracts/proposals/PositionRegistry.cdc
Responsibility: Insurance and stability fee calculation and collection. Extracted from Pool to isolate fee logic and its interaction with reserves.
Key change: FeeCollector computes fee amounts and returns them; the coordinator orchestrates the actual vault withdrawal and deposit through ReserveVaultManager.
Interface: See cadence/contracts/proposals/FeeCollector.cdc
Responsibility: Liquidation validation, DEX price comparison, and execution coordination. Extracted from the 100+ line manualLiquidation method.
Key change: Validation is separated from execution. The engine validates and returns a LiquidationPlan that the coordinator executes.
Interface: See cadence/contracts/proposals/LiquidationEngine.cdc
Before: One PoolState resource with everything.
After:
ReserveVaultManager— vault storage, purpose-specific withdrawalsTokenLedger—{Type: TokenState}, interest accountingPositionRegistry—{UInt64: InternalPosition}, locks, update queueFeeState— insurance fund, stability funds (withinFeeCollector)
Before: collateralFactor and borrowFactor in PoolConfig, everything else in TokenState.
After: Each TokenState includes its own RiskParams. Building a TokenSnapshot only requires one source of truth per token.
Before: InternalBalance.recordDeposit(amount, tokenState) directly mutates tokenState.
After: TokenLedger.applyDeposit(tokenType, positionBalance, amount) returns a BalanceEffect describing what changed. The coordinator applies these effects, making data flow explicit and testable.
// Before (hidden side effects):
position.borrowBalance(type)!.recordDeposit(amount: ..., tokenState: tokenState)
// After (explicit effects):
let effect = tokenLedger.computeDepositEffect(type, balance, amount)
tokenLedger.applyEffect(effect)
positionRegistry.updateBalance(pid, type, effect.newBalance)
Before: Built ad-hoc in 4+ places with slightly different code paths.
After: Single tokenLedger.snapshot(type, riskParams, oraclePrice) factory.
Before: Single EImplementation for all internal operations.
After:
EReserveDeposit/EReserveWithdraw— reserve vault operationsETokenAccounting— interest index and balance updatesEPositionMutation— position balance changesEFeeCollection— fee calculation and collectionEGovernance— (already exists, unchanged)
These changes are interface-level — the underlying math and business logic remain the same. A phased migration could be:
- Phase 1: Extract
ReserveVaultManagerfromPoolState. This is the highest-value change (auditability of fund flows) and has the smallest blast radius. - Phase 2: Colocate risk factors with
TokenState; centralizeTokenSnapshotconstruction. - Phase 3: Extract
FeeCollectorandLiquidationEngine. - Phase 4: Switch to effects-based balance mutations.
- Phase 5: Extract
PositionRegistryand introduce fine-grained entitlements.
Each phase can be validated independently against existing tests.
ARCHITECTURE_REVIEW.md— this documentcadence/contracts/proposals/ReserveVaultManager.cdc— reserve vault interfacecadence/contracts/proposals/TokenLedger.cdc— token-level accounting interfacecadence/contracts/proposals/PositionRegistry.cdc— position management interfacecadence/contracts/proposals/FeeCollector.cdc— fee collection interfacecadence/contracts/proposals/LiquidationEngine.cdc— liquidation engine interfacecadence/contracts/proposals/PoolCoordinator.cdc— thin coordinator showing how modules compose