Skip to content

[Feature] Forked Mainnet Simulation Tests#191

Open
jribbink wants to merge 83 commits intomainfrom
feature/forked-simulations
Open

[Feature] Forked Mainnet Simulation Tests#191
jribbink wants to merge 83 commits intomainfrom
feature/forked-simulations

Conversation

@jribbink
Copy link
Copy Markdown

@jribbink jribbink commented Feb 27, 2026

Description

Feature branch that adds forked rebalance simulations for the real PYUSD0 strategy, mirroring those initially created via mocked bootstrapped tests.


For contributor use:

  • Targeted PR against master branch
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code follows the standards mentioned here.
  • Updated relevant documentation
  • Re-reviewed Files changed in the Github PR explorer
  • Added appropriate labels

RZhang05 and others added 30 commits January 8, 2026 16:21
@jribbink jribbink force-pushed the feature/forked-simulations branch from 4ca1038 to e45967b Compare March 23, 2026 23:12
@jribbink jribbink force-pushed the feature/forked-simulations branch from e45967b to 82d82ed Compare March 26, 2026 01:50
@jribbink jribbink marked this pull request as ready for review March 27, 2026 19:38
@jribbink jribbink requested a review from a team as a code owner March 27, 2026 19:38
RZhang05 and others added 2 commits March 30, 2026 17:18
* Add forked rebalance scenarios 4 and 5.

* Update forked scenarios to match main.

* Update scenarios 4,5.
}

access(all) var testSnapshot: UInt64 = 0
access(all)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
access(all)
// Verify that the YieldVault correctly rebalances yield token holdings when FLOW price changes
access(all)

Comment on lines +157 to +158
// Likely 0.0
let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused variable

Suggested change
// Likely 0.0
let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!
// confirm user exists.
getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!


let flowPrices = [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 5.0]

// Expected values from Google sheet calculations
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you link the google sheet?

EVM.store(target: poolAddr, slot: slotHex(3), value: zero32)

// --- Slot 4: liquidity = uint128 max ---
let liquidityAmount: UInt256 = 340282366920938463463374607431768211455 // 2^128 - 1
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned that we are mocking the uniswap pool in a way that have max liquidity. I understand this is useful to ensure that no matter how much token to swap, the swapped price can always be the targeted price we set.

But in real world, the liquidity is often capped, and we also want to verify the behavior when there isn't enough liquidity.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is a fair concern.

The intitial scope of the forked simulations was to replicate the existing rebalance style tests, as the team wasn't completely that the behaviour would be matched in a real integration-style test against real oracle integrations & EVM pools.

That being said, I agree that the rigidity leaves room for error. Could be worth exploring another style test as a follow-up with more realistic liquidity ticks, but only asserting properties instead of absolute values (e.g. rebalance happens, health factor improves/hits target, realsistic slippage values don't affect system negatively) rather than exact values. Open to ideas on what that should look like.

@@ -0,0 +1,294 @@
#test_fork(network: "mainnet-fork", height: 143292255)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference in the setup between this forked mainnet and rebalance_scenario1_test.cdc?

What is covered in this tests but is not covered in there?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main difference is just using the contracts deployed on mainnet instead of the mocked ones (e.g. MockSwapper, MockOracle) in the original rebalance_scenario*.cdc. Otherwise, the tests are identical by design and the expected values are exactly the same.

let expectedYieldTokenValues: {UFix64: UFix64} = {
0.5: 307.69230769,
0.8: 492.30769231,
1.0: 615.38461538,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically value = key * 615.38461538, for instance 5.0: (5.0 * 615.38461538) = 3076.92307692.

So how does the rebalancing works in this tests? I'm not sure I understand the specific case we are testing here.

When flow price drops to 0.5, it means the yield vault price is dropped for the same ratio (50% off). Then the rebalance on both the yield vault and the position is for the yield vault value to realize it's value dropped for 50%?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think I got it. suggested the above comments


let flowPrices = [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 5.0]

// Expected values from Google sheet calculations
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Expected values from Google sheet calculations
// ============================================================================
// REBALANCING MATH FOR FUSDEVStrategy
// ============================================================================
//
// Initial Setup:
// - Collateral: 1000 FLOW at $1.0 = $1000
// - LTV (Loan-to-Value): 80% (0.8)
// - Borrow-Eligible: 1000 * 1.0 * 0.8 = $800
// - Target Health Factor: 1.3
// - Initial Debt: 800 / 1.3 = 615.384615385 MOET
// - Initial HF: (1000 * 1.0 * 0.8) / 615.384615385 = 1.300
//
// Rebalancing Triggers:
// - HF < 1.1: Position is under-collateralized → Repay debt
// - HF > 1.5: Position is over-collateralized → Borrow more
// - Rebalancing restores HF to target 1.3
//
// Trigger Calculation (with initial debt 615.384615385):
// - HF < 1.1 when: 1000 * X * 0.8 / 615.384615385 < 1.1 → X < 0.84615
// - HF > 1.5 when: 1000 * X * 0.8 / 615.384615385 > 1.5 → X > 1.15385
//
// Expected Values Table (after rebalancing to HF = 1.3):
// ┌────────────┬────────────┬───────────────┬───────────────┬──────────────┬───────────────────┬─────────────┬─────────────┬─────────────┐
// │ FLOW Price │ Collateral │ Borrow-Elig. │ Debt Before │ HF Before │ Action │ Debt After │ YIELD After │ HF After │
// ├────────────┼────────────┼───────────────┼───────────────┼──────────────┼───────────────────┼─────────────┼─────────────┼─────────────┤
// │ 0.50 │ 500 │ 400 │ 615.38 │ 0.65 │ Repay 307.69 │ 307.69 │ 307.69 │ 1.30 │
// │ 0.80 │ 800 │ 640 │ 615.38 │ 1.04 │ Repay 123.08 │ 492.31 │ 492.31 │ 1.30 │
// │ 1.00 │ 1000 │ 800 │ 615.38 │ 1.30 │ none │ 615.38 │ 615.38 │ 1.30 │
// │ 1.20 │ 1200 │ 960 │ 615.38 │ 1.56 │ Borrow 123.08 │ 738.46 │ 738.46 │ 1.30 │
// │ 1.50 │ 1500 │ 1200 │ 615.38 │ 1.95 │ Borrow 307.69 │ 923.08 │ 923.08 │ 1.30 │
// │ 2.00 │ 2000 │ 1600 │ 615.38 │ 2.60 │ Borrow 615.38 │ 1230.77 │ 1230.77 │ 1.30 │
// │ 3.00 │ 3000 │ 2400 │ 615.38 │ 3.90 │ Borrow 1230.77 │ 1846.15 │ 1846.15 │ 1.30 │
// │ 5.00 │ 5000 │ 4000 │ 615.38 │ 6.50 │ Borrow 2461.54 │ 3076.92 │ 3076.92 │ 1.30 │
// └────────────┴────────────┴───────────────┴───────────────┴──────────────┴───────────────────┴─────────────┴─────────────┴─────────────┘
//
// Formula: New Debt = Borrow-Eligible / Target HF = (Collateral * LTV) / 1.3
// ============================================================================

let expectedYieldTokenValues: {UFix64: UFix64} = {
0.5: 307.69230769,
0.8: 492.30769231,
1.0: 615.38461538,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think I got it. suggested the above comments

Comment on lines +194 to +195
rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false)
rebalancePosition(signer: flowALPAccount, pid: pid, force: true, beFailed: false)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to both rebalancing? I think we only need to rebalance the position, because there is only flow price change, no yield vault price change. So rebalanceYieldVault should be a noop. And rebalancePosition will trigger either borrow more MOET and convert to yield token (HF > 1.5) or sell yield token and pay MOET (HF < 1.1)

Suggested change
rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false)
rebalancePosition(signer: flowALPAccount, pid: pid, force: true, beFailed: false)
rebalancePosition(signer: flowALPAccount, pid: pid, force: true, beFailed: false)

I tested by removing rebalanceYieldVault, and the tests still pass. But if I remove rebalancePosition the tests fail.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agree. This was something we had just copied over from the original tests, but we should remove it, like in #208.


let user = Test.createAccount()

let yieldPriceIncreases = [1.1, 1.2, 1.3, 1.5, 2.0, 3.0]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let yieldPriceIncreases = [1.1, 1.2, 1.3, 1.5, 2.0, 3.0]
// ===================================================================================
// REBALANCING SIMULATION: YIELD price appreciation with constant Health Factor 1.3
// ===================================================================================
//
// Thresholds:
// AutoBalancer: lowerThreshold=0.95, upperThreshold=1.05 (±5% of base value)
// Position: minHealth=1.1, targetHealth=1.3, maxHealth=1.5
//
// Initial State (Price=$1.00):
// Collateral: 1000 FLOW
// Debt: 1000 × 0.8 / 1.3 = 615.38 (borrowed to buy YIELD tokens)
// YIELD Units: 615.38 (purchased with debt at $1.00)
// Baseline: $615.38 (AutoBalancer's valueOfDeposits, equals initial debt)
// Health: 1.3
//
// When YIELD Price increases (e.g. $1.00 → $1.10):
//
// 1. TRIGGER: Price change
// YIELD Value = 615.38 × $1.10 = $676.92
// Value Ratio = $676.92 / $615.38 = 110%
// Exceeds AutoBalancer upperThreshold (1.05) → rebalance triggered
//
// 2. AUTOBALANCER ACTION: Sell YIELD to bring value back to baseline
// Value Diff = $676.92 - $615.38 = $61.54 (surplus above baseline)
// Bal Sell = Value Diff / Price = $61.54 / $1.10 = 55.94 units
// Remaining YIELD = 615.38 - 55.94 = 559.44 units (worth $615.38 = baseline)
// Profit $61.54 added to Collateral: 1000 + 61.54 = 1061.54
//
// 3. POSITION REBALANCE: Borrow more (higher collateral supports more debt)
// Target Debt = 1061.54 × 0.8 / 1.3 = 653.25
// Borrow: 653.25 - 615.38 = 37.87
// YIELD Bought: 37.87 / $1.10 = 34.43 units
//
// 4. FINAL STATE:
// YIELD Units: 559.44 + 34.43 = 593.87
// Debt: 653.25
// Collateral: 1061.54
// Health: 1.3 (maintained)
//
// Key Formulas:
// Bal Sell = (Prev Units × New Price - Baseline) / New Price
// = Prev Units - (Baseline / New Price)
// Profit = Bal Sell × Price
// New Collateral = Prev Collateral + Profit
// Target Debt = Collateral × 0.8 / 1.3
// YIELD Bought = Borrow / Price
// Final YIELD Units = (Prev Units - Bal Sell) + YIELD Bought
//
// -----------------------------------------------------------------------------------
// Price | Debt | YIELD Units | Collateral | Health | Actions
// -----------------------------------------------------------------------------------
// 1.00 | 615.38 | 615.38 | 1000.00 | 1.30 | (initial)
// 1.10 | 653.25 | 593.87 | 1061.54 | 1.30 | Sell 55.94 | Borrow 37.87
// 1.20 | 689.80 | 574.83 | 1120.93 | 1.30 | Sell 49.49 | Borrow 36.55
// 1.30 | 725.17 | 557.83 | 1178.41 | 1.30 | Sell 44.22 | Borrow 35.37
// 1.50 | 793.83 | 529.22 | 1289.97 | 1.30 | Sell 74.38 | Borrow 68.66
// 2.00 | 956.67 | 478.33 | 1554.58 | 1.30 | Sell 132.31 | Borrow 162.84
// 3.00 | 1251.03 | 417.01 | 2032.92 | 1.30 | Sell 159.44 | Borrow 294.36
// -----------------------------------------------------------------------------------
//
// ===================================================================================
let yieldPriceIncreases = [1.1, 1.2, 1.3, 1.5, 2.0, 3.0]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice in step 2 the rebalancer sells 55.94 yield token, and then in step 3 it buys back 34.43 yield token. It would be nicer to only sell the diff, but probably more complex to implement.

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