Skip to content

Commit aa6a844

Browse files
Merged
2 parents 1e7ebce + 5076b21 commit aa6a844

73 files changed

Lines changed: 6451 additions & 1337 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/cadence_tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ jobs:
2828
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
2929
restore-keys: |
3030
${{ runner.os }}-go-
31+
- uses: actions/cache@v4
32+
with:
33+
path: ./imports
34+
key: flow-deps-${{ hashFiles('flow.json') }}
3135
- name: Install Flow CLI
3236
env:
3337
FLOW_CLI_VERSION: v2.7.2

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ coverage.json
77
/.pr-body.md
88
/.github/pr_bodies/
99
lcov.info
10+
.flow-fork-cache

PriceOracleArchitecture.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Price Oracle Architecture
2+
3+
This document describes the price oracle design for the ALP.
4+
How multiple sources are combined into a single trusted oracle interface, and how routing and aggregation are split across two contracts.
5+
6+
## Overview
7+
8+
The protocol depends on a **single trusted oracle** that returns either a valid price or `nil` when the price should not be used (e.g. liquidation or rebalancing should be skipped). The protocol does **not** validate prices; it only consumes the oracle’s result.
9+
10+
Two contracts implement this design:
11+
12+
| Contract | Role |
13+
|----------|------|
14+
| **PriceOracleAggregatorv1** | Combines multiple price sources for **one** market (e.g. several FLOW/USDC oracles). Returns a price only when sources agree within spread tolerance and short-term history is within `baseTolerance` + `driftExpansionRate` (stability). |
15+
| **PriceOracleRouterv1** | Exposes **one** `DeFiActions.PriceOracle` that routes by token type. Each token has its own oracle; typically each oracle is an aggregator. |
16+
17+
Typical usage: create one **aggregator** per market (same token pair, multiple sources), then register each aggregator in a **router** under the corresponding token type. The protocol then uses the router as its single oracle.
18+
19+
### Immutable Configuration
20+
21+
The **Aggregator** and **Router** are immutable by design to eliminate the risks associated with live production changes.
22+
23+
* **Eliminates "Testing in Prod":** Because parameters cannot be modified in place, you avoid the risk of breaking a live oracle. Instead, new configurations can be fully tested as a separate instance before deployment.
24+
* **Centralized Governance:** Changes can only be made by updating the oracle reference on the **ALP**. This makes it explicitly clear who holds governance authority over the system.
25+
* **Timelock Compatibility:** Since updates require a fresh deployment, it is easy to implement an "Escape Period" (Timelock). This introduces a mandatory delay before a new oracle address takes effect, giving users time to react or exit before the change goes live.
26+
* **Transparent Auditing:** Every change is recorded on-chain via the `PriceOracleUpdated` event, ensuring all shifts in logic or parameters are visible and expected.
27+
28+
## PriceOracleAggregatorv1
29+
30+
One aggregated oracle per “market” (e.g. FLOW in USDC). Multiple underlying oracles, single unit of account, fixed tolerances.
31+
- **Price flow:**
32+
1. Collect prices from all oracles for the requested token.
33+
2. If any oracle returns nil → emit `PriceNotAvailable`, return nil.
34+
3. Compute min/max; if spread > `maxSpread` → emit `PriceNotWithinSpreadTolerance`, return nil.
35+
4. Compute aggregated price as the arithmetic mean of all oracle prices.
36+
5. Check short-term stability: compare current price to recent history; for each history entry the allowed relative difference is `baseTolerance + driftExpansionRate * deltaTMinutes`; if any relative difference exceeds that → emit `PriceNotWithinHistoryTolerance`, return nil.
37+
6. Otherwise return the aggregated price.
38+
- **History:** An array of `(price, timestamp)` is maintained. Updates are permissionless via `tryAddPriceToHistory()` (idempotent); A FlowCron job should be created to call this regularly.
39+
Additionally every call to price() will also attempt to store the price in the history.
40+
41+
## Aggregate price (average)
42+
43+
The aggregator uses the **arithmetic mean** of all oracle prices:
44+
45+
- **Average:** `sum(prices) / count`. Same for any number of oracles (1, 2, 3+).
46+
47+
## Oracle spread (coherence)
48+
49+
A **pessimistic relative spread** is used: the distance between the most extreme oracle prices relative to the **minimum** price.
50+
51+
$$
52+
\text{Spread} = \frac{Price_{\max} - Price_{\min}}{Price_{\min}}
53+
$$
54+
55+
The price set is **coherent** only if:
56+
57+
$$
58+
\text{isCoherent} =
59+
\begin{cases}
60+
\text{true} & \text{if } \frac{Price_{\max} - Price_{\min}}{Price_{\min}} \le maxSpread \\
61+
\text{false} & \text{otherwise}
62+
\end{cases}
63+
$$
64+
65+
## Short-term stability (history tolerance)
66+
67+
The aggregator keeps an array of the last **n** aggregated prices (with timestamps), respecting `priceHistoryInterval` and `maxPriceHistoryAge`.
68+
69+
Stability is defined by two parameters:
70+
71+
- **baseTolerance** (n): fixed buffer to account for immediate market noise.
72+
- **driftExpansionRate** (m): additional allowance per minute to account for natural price drift.
73+
74+
For each historical point (i), the **allowed relative difference** between the current price and the history price grows with time:
75+
76+
$$
77+
\text{allowedRelativeDiff}_{i} = \text{baseTolerance} + \text{driftExpansionRate} \times \Delta t_{\text{minutes}}
78+
$$
79+
80+
where Delta t_minutes is the time in minutes from the history entry to now. The **actual relative difference** is:
81+
82+
$$
83+
\text{relativeDiff}_{i} = \frac{|Price_{\text{current}} - Price_{i}|}{\min(Price_{\text{current}}, Price_{i})}
84+
$$
85+
86+
The current price is **stable** only if **every** such relative difference (from each valid history entry to the current price) is at or below the allowed tolerance for that entry. If **any** exceeds it, the aggregator emits `PriceNotWithinHistoryTolerance(relativeDiff, deltaTMinutes, maxAllowedRelativeDiff)` and returns nil.
87+
88+
$$
89+
\text{isStable} =
90+
\begin{cases}
91+
\text{true} & \text{if } \text{relativeDiff}_{i} \le \text{allowedRelativeDiff}_{i} \text{ for all } i \\
92+
\text{false} & \text{otherwise (price invalid)}
93+
\end{cases}
94+
$$
95+
96+
Implementationally, entries older than `maxPriceHistoryAge` are ignored when evaluating stability.
97+
98+
**Parameter units:** `maxSpread`, `baseTolerance`, and `driftExpansionRate` are dimensionless relative values (e.g. `0.01` = 1%, `1.0` = 100%). All are bounded by the contract to ≤ 10000.0.
99+
100+
## PriceOracleRouterv1
101+
102+
Single oracle interface that routes by **token type**. Each token type maps to an oracle. This makes it easy to combine different aggregators without the need to supply different kinds of thresholds for individual token types.
103+
104+
- **Price flow:** `price(ofToken)` looks up the oracle for that token type; if none is registered, returns `nil`. All oracles must share the same `unitOfAccount` (enforced at router creation).
105+
- **Empty router:** If the oracle map is empty or a token type is not registered, `price(ofToken)` returns `nil`.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
## 🏦 About FlowALP
3838

39-
FlowALP is a decentralized lending and borrowing protocol built on the Flow blockchain. This repository contains the v1 Cadence implementation deployed as the `FlowALPv0` contract. It is token-agnostic (operates over any `FungibleToken.Vault`) and integrates with DeFi Actions for composability.
39+
FlowALP is a decentralized lending and borrowing protocol built on the Flow blockchain. This repository contains the current Cadence implementation deployed as the `FlowALPv0` contract. It is token-agnostic (operates over any `FungibleToken.Vault`) and integrates with DeFi Actions for composability.
4040

4141
### Key Features
4242

RebalanceArchitecture.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
## Updated Rebalance Architecture
22

3-
This system **rebalances Flow Credit Market (FCM) positions on a schedule**: at a configurable interval, a rebalancer triggers the position’s `rebalance` function. **FCM** holds positions and exposes `rebalance`.
3+
This system **rebalances FlowALP positions on a schedule**: at a configurable interval, a rebalancer triggers the position’s `rebalance` function. **FlowALP** holds positions and exposes `rebalance`.
4+
5+
**Implementation note:** In the current implementation, the FlowALP pool is `FlowALPv0.Pool`.
46

57
A **Rebalancer** when invoked, calls `rebalance` on the position and tries to schedules the next run.
68

79
A **Supervisor** runs on its own schedule (cron) and calls `fixReschedule()` on each registered rebalancer so that transient scheduling failures (e.g. temporary lack of funds) don’t leave rebalancers stuck.
810

911
### Key Principles
1012

11-
* **Isolation:** FCM, Rebalancer, and Supervisor are fully independent.
13+
* **Isolation:** FlowALP, Rebalancer, and Supervisor are fully independent.
1214
* **Least Privilege:** The Rebalancer can *only* trigger the `rebalance` function.
1315
* **Resilience:** The `fixReschedule()` call is idempotent and permissionless, ensuring the system can recover without complex auth (see below).
1416

@@ -39,7 +41,7 @@ There are two rebalancer types; they behave the same for triggering rebalances;
3941
| **User’s control** | Full: config, fixReschedule, withdraw/destroy | Only: fixReschedule by UUID, or delete their RebalancerPaid (stops and removes the rebalancer) |
4042
| **Use case** | User wants full autonomy and to pay their own fees | Admin retains autonomy and pays fees for users (us only) |
4143

42-
**Note:** The Supervisor and the Paid Rebalancer are only intended for use by us; the Standard Rebalancer is for users who self-custody. The bundled `FlowALPSupervisorV1` only tracks **paid** rebalancers (`addPaidRebalancer` / `removePaidRebalancer`). For standard rebalancers, users can call `fixReschedule()` themselves when needed.
44+
**Note:** The Supervisor and the Paid Rebalancer are only intended for use by us; the Standard Rebalancer is for users who self-custody. The bundled `FlowALPSupervisorv1` only tracks **paid** rebalancers (`addPaidRebalancer` / `removePaidRebalancer`). For standard rebalancers, users can call `fixReschedule()` themselves when needed.
4345

4446
### Why calls `fixReschedule()` are necessary
4547

@@ -57,12 +59,12 @@ User creates a position, then creates a **paid** rebalancer (which lives in the
5759
sequenceDiagram
5860
actor admin
5961
actor User
60-
participant FCM
62+
participant FlowALP
6163
participant Paid as Paid Rebalancer Contract
6264
participant Supervisor
6365
Note over admin,Paid: One-time: admin sets defaultRecurringConfig (incl. txFunder)
6466
admin->>Paid: updateDefaultRecurringConfig(config)
65-
User->>FCM: createPosition()
67+
User->>FlowALP: createPosition()
6668
User->>Paid: createPaidRebalancer(positionRebalanceCapability)
6769
Paid-->>User: RebalancerPaid(uuid)
6870
User->>User: save RebalancerPaid
@@ -87,17 +89,17 @@ sequenceDiagram
8789
```mermaid
8890
sequenceDiagram
8991
participant AB1 as AutoRebalancer1
90-
participant FCM
92+
participant FlowALP
9193
participant AB2 as AutoRebalancer2
9294
participant SUP as Supervisor
9395
loop every x min
94-
AB1->>FCM: rebalance()
96+
AB1->>FlowALP: rebalance()
9597
end
9698
loop every y min
97-
AB2->>FCM: rebalance()
99+
AB2->>FlowALP: rebalance()
98100
end
99101
loop every z min
100102
SUP->>AB2: fixReschedule()
101103
SUP->>AB1: fixReschedule()
102104
end
103-
```
105+
```

0 commit comments

Comments
 (0)