Skip to content

Commit 5f036bd

Browse files
jribbinkclaude
andcommitted
Fix non-fork test compatibility: use V1 scripts/transactions only for fork tests
- Reverted shared scripts and transactions back to FlowYieldVaultsAutoBalancers - Created fork-specific V1 scripts in cadence/tests/scripts/ - Created fork-specific V1 rebalance transaction in cadence/tests/transactions/ - Added isForkTest flag to test_helpers to select correct paths - BandOracleConnectors deployment gated behind skipBreakingChanges - Refreshed oracle prices before seedPoolWithPYUSD0 in scenario4 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7a9cbbe commit 5f036bd

9 files changed

Lines changed: 117 additions & 12 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import "FlowYieldVaultsAutoBalancersV1"
1+
import "FlowYieldVaultsAutoBalancers"
22

33
/// Returns the balance of the AutoBalancer related to the provided YieldVault ID or `nil` if none exists
44
///
55
access(all)
66
fun main(id: UInt64): UFix64? {
7-
return FlowYieldVaultsAutoBalancersV1.borrowAutoBalancer(id: id)?.vaultBalance()
7+
return FlowYieldVaultsAutoBalancers.borrowAutoBalancer(id: id)?.vaultBalance()
88
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import "FlowYieldVaultsAutoBalancersV1"
1+
import "FlowYieldVaultsAutoBalancers"
22

33
/// Returns the current value of the AutoBalancer's balance related to the provided YieldVault ID or `nil` if none exists
44
///
55
access(all)
66
fun main(id: UInt64): UFix64? {
7-
return FlowYieldVaultsAutoBalancersV1.borrowAutoBalancer(id: id)?.currentValue() ?? nil
7+
return FlowYieldVaultsAutoBalancers.borrowAutoBalancer(id: id)?.currentValue() ?? nil
88
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Rebalance Boundary Behavior Analysis
2+
3+
## Context
4+
5+
When the yield token (FUSDEV) price drops, the FYV AutoBalancer detects a deficit (yield value < baseline) and sells collateral (FLOW) to buy more yield tokens. This test validates the boundary behavior at the 0.95 lower threshold.
6+
7+
## Observed Behavior
8+
9+
When the YT price drops significantly (e.g., to P=0.10):
10+
11+
1. **FYV AutoBalancer deficit recovery**: The AB sells FLOW collateral → PYUSD0 → FUSDEV to cover the deficit. The collateral withdrawal is capped by the position's `availableBalance(pullFromTopUpSource: false)`, which limits withdrawal to what maintains H ≥ minHealth (1.1). This pulls the position down to exactly H ≈ 1.1.
12+
13+
2. **FlowALP position does NOT rebalance**: After the AB pull, H lands at ~1.10000000003 (due to integer rounding). The position's rebalance check `minHealth <= health` evaluates to `1.1 <= 1.10000000003 → true` → position considers itself "in bounds" and does NOT rebalance to restore H=1.3.
14+
15+
3. **State freezes**: No further deficit recovery is possible because:
16+
- The AB has no collateral room to pull (position is at minHealth)
17+
- The position doesn't rebalance (H rounds to "in bounds")
18+
- The yield deficit remains permanently unresolved
19+
20+
## Systemic Risk
21+
22+
This creates a tendency to leave positions **chronically at minimum health** (H ≈ 1.1):
23+
24+
- **Thin liquidation buffer**: H=1.1 has only a 10% buffer above liquidation (H=1.0). A modest collateral price drop (~9%) would push into liquidation territory.
25+
26+
- **Value destruction during deficit recovery**: The AB buys yield tokens that are deeply underwater. At P=0.10, each FLOW of collateral buys FUSDEV worth only 0.10 PYUSD0 — a 90% loss. This collateral is effectively wasted since the position rebalance would immediately sell those yield tokens back to repay debt if it fired.
27+
28+
- **No self-healing**: Without an external `force=true` rebalance call, the position stays frozen at minHealth indefinitely. The scheduled rebalancer uses `force=false` and won't trigger.
29+
30+
## Root Cause
31+
32+
The FYV AutoBalancer and FlowALP position rebalancer work against each other:
33+
34+
| Step | Actor | Action | Effect |
35+
|------|-------|--------|--------|
36+
| 1 | FYV AB | Sells collateral → buys yield | C decreases, U increases, H drops to 1.1 |
37+
| 2 | ALP Position | Should sell yield → repay debt | Would restore H to 1.3, freeing room |
38+
| - | ALP Position | **Does NOT fire** | H=1.10000000003 ≥ minHealth (1.1) → "in bounds" |
39+
40+
If the position DID rebalance, it would sell yield → repay debt → H restores to 1.3 → frees collateral room → AB could continue deficit recovery. This would converge in 2-3 rounds. But the rounding prevents step 2 from ever firing.
41+
42+
## Potential Solutions
43+
44+
1. **`pullFromTopUpSource: true` on the AB's PositionSource**: The AB pull would atomically sell yield → repay debt → free collateral → withdraw, all in one step. Single-round convergence with no wasteful collateral-to-yield roundtrip.
45+
46+
2. **Health epsilon on lower bound**: Add `minHealth + epsilon <= health` check so the position rebalances when H is "effectively at" minHealth. This enables multi-round convergence but still involves the wasteful collateral→yield→debt roundtrip.
47+
48+
3. **Skip AB deficit when yield is deeply underwater**: If yield value / baseline < some threshold (e.g., 0.5), the AB could skip deficit recovery entirely and let the position deleverage directly. Avoids buying deeply depreciating assets.
49+
50+
4. **Direct deleverage path**: Instead of selling collateral→yield, the AB could signal the position to reduce leverage (sell yield → repay debt) without touching collateral. The net effect is the same as what the position rebalance does, but without the collateral roundtrip.
51+
52+
## Current Test Behavior
53+
54+
The boundary test shows:
55+
- P=0.96: no rebalance (ratio > 0.95) ✓
56+
- P=0.95: AB deficit fires, pulls collateral (H: 1.30 → 1.26) ✓
57+
- P=0.94: no change — baseline updated after P=0.95, new ratio > 0.95 ✓
58+
- P=0.10: AB pulls collateral to H≈1.1, then **frozen** — position doesn't rebalance, deficit unresolved
59+
60+
The scenario2 DOWN phase shows the same freezing: after the first partial deficit at P=2.0 pulls to H≈1.1, all subsequent yield price drops produce no further state changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import FlowYieldVaultsAutoBalancersV1 from 0xb1d63873c3cc9f79
2+
3+
access(all)
4+
fun main(id: UInt64): UFix64? {
5+
return FlowYieldVaultsAutoBalancersV1.borrowAutoBalancer(id: id)?.vaultBalance()
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import FlowYieldVaultsAutoBalancersV1 from 0xb1d63873c3cc9f79
2+
3+
access(all)
4+
fun main(id: UInt64): UFix64? {
5+
return FlowYieldVaultsAutoBalancersV1.borrowAutoBalancer(id: id)?.currentValue() ?? nil
6+
}

cadence/tests/test_helpers.cdc

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import "FlowYieldVaults"
99

1010
access(all) let serviceAccount = Test.serviceAccount()
1111

12+
/// Set to true by deployContractsForFork() — used to select V1 AutoBalancer scripts
13+
access(all) var isForkTest = false
14+
1215
access(all) struct DeploymentConfig {
1316
access(all) let uniswapFactoryAddress: String
1417
access(all) let uniswapRouterAddress: String
@@ -211,6 +214,7 @@ access(all) fun deployContracts() {
211214
}
212215

213216
access(all) fun deployContractsForFork() {
217+
isForkTest = true
214218
let config = DeploymentConfig(
215219
uniswapFactoryAddress: "0xca6d7Bb03334bBf135902e1d919a5feccb461632",
216220
uniswapRouterAddress: "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341",
@@ -541,14 +545,21 @@ fun getYieldVaultDisplayView(address: Address, yieldVaultID: UInt64): MetadataVi
541545

542546
access(all)
543547
fun getAutoBalancerBalance(id: UInt64): UFix64? {
544-
let res = _executeScript("../scripts/flow-yield-vaults/get_auto_balancer_balance_by_id.cdc", [id])
548+
// Fork tests use FlowYieldVaultsAutoBalancersV1 (mainnet contract); non-fork use FlowYieldVaultsAutoBalancers
549+
let path = isForkTest
550+
? "scripts/get_auto_balancer_balance_v1.cdc"
551+
: "../scripts/flow-yield-vaults/get_auto_balancer_balance_by_id.cdc"
552+
let res = _executeScript(path, [id])
545553
Test.expect(res, Test.beSucceeded())
546554
return res.returnValue as! UFix64?
547555
}
548556

549557
access(all)
550558
fun getAutoBalancerCurrentValue(id: UInt64): UFix64? {
551-
let res = _executeScript("../scripts/flow-yield-vaults/get_auto_balancer_current_value_by_id.cdc", [id])
559+
let path = isForkTest
560+
? "scripts/get_auto_balancer_current_value_v1.cdc"
561+
: "../scripts/flow-yield-vaults/get_auto_balancer_current_value_by_id.cdc"
562+
let res = _executeScript(path, [id])
552563
Test.expect(res, Test.beSucceeded())
553564
return res.returnValue as! UFix64?
554565
}
@@ -767,7 +778,10 @@ fun withdrawFromYieldVault(signer: Test.TestAccount, id: UInt64, amount: UFix64,
767778

768779
access(all)
769780
fun rebalanceYieldVault(signer: Test.TestAccount, id: UInt64, force: Bool, beFailed: Bool) {
770-
let res = _executeTransaction("../transactions/flow-yield-vaults/admin/rebalance_auto_balancer_by_id.cdc", [id, force], signer)
781+
let path = isForkTest
782+
? "transactions/rebalance_auto_balancer_v1.cdc"
783+
: "../transactions/flow-yield-vaults/admin/rebalance_auto_balancer_by_id.cdc"
784+
let res = _executeTransaction(path, [id, force], signer)
771785
Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded())
772786
}
773787

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import AutoBalancers from 0xb1d63873c3cc9f79
2+
import FlowYieldVaultsAutoBalancersV1 from 0xb1d63873c3cc9f79
3+
4+
transaction(id: UInt64, force: Bool) {
5+
let autoBalancer: auth(AutoBalancers.Auto) &AutoBalancers.AutoBalancer
6+
7+
prepare(signer: auth(BorrowValue) &Account) {
8+
let storagePath = FlowYieldVaultsAutoBalancersV1.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath
9+
self.autoBalancer = signer.storage.borrow<auth(AutoBalancers.Auto) &AutoBalancers.AutoBalancer>(from: storagePath)
10+
?? panic("Could not borrow reference to AutoBalancer id \(id) at path \(storagePath)")
11+
}
12+
13+
execute {
14+
self.autoBalancer.rebalance(force: force)
15+
}
16+
}

cadence/transactions/flow-yield-vaults/admin/rebalance_auto_balancer_by_id.cdc

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
import "AutoBalancers"
1+
import "DeFiActions"
22

3-
import "FlowYieldVaultsAutoBalancersV1"
3+
import "FlowYieldVaultsAutoBalancers"
44

55
/// Calls on the AutoBalancer to rebalance which will result in a rebalancing around the value of deposits. If force is
66
/// `true`, rebalancing should occur regardless of the lower & upper thresholds configured on the AutoBalancer.
77
/// Otherwise, rebalancing will only occur if the value of deposits is above or below the relative thresholds and
88
/// a rebalance Sink or Source is set.
99
///
10+
/// For more information on DeFiActions AutoBalancers, see the DeFiActions contract.
11+
///
1012
/// @param id: The YieldVault ID for which the AutoBalancer is associated
1113
/// @param force: Whether or not to force rebalancing, bypassing it's thresholds for automatic rebalancing
1214
///
1315
transaction(id: UInt64, force: Bool) {
1416
// the AutoBalancer that will be rebalanced
15-
let autoBalancer: auth(AutoBalancers.Auto) &AutoBalancers.AutoBalancer
17+
let autoBalancer: auth(DeFiActions.Auto) &DeFiActions.AutoBalancer
1618

1719
prepare(signer: auth(BorrowValue) &Account) {
1820
// derive the path and borrow an authorized reference to the AutoBalancer
19-
let storagePath = FlowYieldVaultsAutoBalancersV1.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath
20-
self.autoBalancer = signer.storage.borrow<auth(AutoBalancers.Auto) &AutoBalancers.AutoBalancer>(from: storagePath)
21+
let storagePath = FlowYieldVaultsAutoBalancers.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath
22+
self.autoBalancer = signer.storage.borrow<auth(DeFiActions.Auto) &DeFiActions.AutoBalancer>(from: storagePath)
2123
?? panic("Could not borrow reference to AutoBalancer id \(id) at path \(storagePath)")
2224
}
2325

lib/FlowCreditMarket

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 74f939bcb795b03c733758d74c76f9eaa72455f5

0 commit comments

Comments
 (0)