Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e1007b2
Recreate scenario 1 using forked testnet.
RZhang05 Jan 8, 2026
351578c
Remove unused function.
RZhang05 Jan 8, 2026
5ff2bd0
Update for mainnet addresses.
RZhang05 Jan 9, 2026
3068055
Use mUSDC strategy with real oracles on testnet.
RZhang05 Jan 12, 2026
deb846e
Update symbol prices.
RZhang05 Jan 12, 2026
f197957
Switch to mainnet.
RZhang05 Jan 14, 2026
a719c67
Merge branch 'main' into raymond/forked-scen1
RZhang05 Jan 14, 2026
d52baf9
Add option to transfer flow from whale account instead of minting.
RZhang05 Jan 20, 2026
44a77a6
Merge branch 'nialexsan/pyusd-fyv-strategy' into raymond/forked-scen1
RZhang05 Jan 26, 2026
0d6b270
Use fixed mainnet height, new FUSDEV strategy.
RZhang05 Jan 26, 2026
2e2400c
Add percent tolerance check to forked test.
RZhang05 Jan 26, 2026
fc67c98
Clean up forked scenario 1.
RZhang05 Jan 27, 2026
f2ea7b3
Remove unnecessary imports.
RZhang05 Jan 28, 2026
eae9d1e
Add EVM State Manipulation Helpers For Forked Simulations
jribbink Feb 13, 2026
0ba2dd2
Merge remote-tracking branch 'origin/main' into jribbink/evm-test-hel…
jribbink Feb 18, 2026
c405a2c
Fix bugs with decimal offsets & cleanup
jribbink Feb 20, 2026
af63f67
tidy comments
jribbink Feb 20, 2026
bb17527
Cleanup test helpers & fix precision
jribbink Feb 22, 2026
5dc80b4
Merge remote-tracking branch 'origin/main' into jribbink/evm-test-hel…
jribbink Feb 22, 2026
fb8b266
Update to newer CLI
jribbink Feb 23, 2026
dc143d0
address feedback
jribbink Feb 23, 2026
5939f17
Add test for EVM state helpers
jribbink Feb 24, 2026
4c1df74
cleanup test assertion
jribbink Feb 24, 2026
d88f651
cleanup test assertion
jribbink Feb 24, 2026
4f6c322
Fix percent tolerance
jribbink Feb 24, 2026
8829009
Merge branch 'feature/forked-simulations' into jribbink/evm-test-helpers
jribbink Feb 24, 2026
875ff38
Merge branch 'main' into raymond/forked-scen1
RZhang05 Feb 24, 2026
845507f
Merge branch 'jribbink/evm-test-helpers' into raymond/forked-scen1
RZhang05 Feb 24, 2026
48a4275
Update to new strategy and use new evm helpers.
RZhang05 Feb 24, 2026
e7a6683
Clean up.
RZhang05 Feb 24, 2026
7ab7e9c
Merge branch 'feature/forked-simulations' into raymond/forked-scen1
RZhang05 Feb 25, 2026
d95de5c
Fix merge.
RZhang05 Feb 25, 2026
2a42e2b
Normalize comments.
RZhang05 Feb 25, 2026
d0c7462
Temporarily skip closing of yield vault.
RZhang05 Feb 25, 2026
f2e72c6
Apply suggestions from code review
RZhang05 Feb 26, 2026
ecfe8e7
Switch flow.json back to tab spacing.
RZhang05 Feb 26, 2026
d4b1fc1
Merge branch 'raymond/forked-scen1' of https://github.com/onflow/Flow…
RZhang05 Feb 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 290 additions & 0 deletions cadence/tests/forked_rebalance_scenario1_test.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
#test_fork(network: "mainnet-fork", height: 143292255)

import Test
import BlockchainHelpers

import "test_helpers.cdc"
import "evm_state_helpers.cdc"

// FlowYieldVaults platform
import "FlowYieldVaults"
// other
import "FlowToken"
import "MOET"
import "FlowYieldVaultsStrategiesV2"
import "FlowALPv0"

// ============================================================================
// CADENCE ACCOUNTS
// ============================================================================

access(all) let flowYieldVaultsAccount = Test.getAccount(0xb1d63873c3cc9f79)
access(all) let flowALPAccount = Test.getAccount(0x6b00ff876c299c61)
access(all) let bandOracleAccount = Test.getAccount(0x6801a6222ebf784a)
access(all) let whaleFlowAccount = Test.getAccount(0x92674150c9213fc9)
access(all) let coaOwnerAccount = Test.getAccount(0xe467b9dd11fa00df)

access(all) var strategyIdentifier = Type<@FlowYieldVaultsStrategiesV2.FUSDEVStrategy>().identifier
access(all) var flowTokenIdentifier = Type<@FlowToken.Vault>().identifier
access(all) var moetTokenIdentifier = Type<@MOET.Vault>().identifier

// ============================================================================
// PROTOCOL ADDRESSES
// ============================================================================

// Uniswap V3 Factory on Flow EVM mainnet
access(all) let factoryAddress = "0xca6d7Bb03334bBf135902e1d919a5feccb461632"

// ============================================================================
// VAULT & TOKEN ADDRESSES
// ============================================================================

// FUSDEV - Morpho VaultV2 (ERC4626)
// Underlying asset: PYUSD0
access(all) let morphoVaultAddress = "0xd069d989e2F44B70c65347d1853C0c67e10a9F8D"

// PYUSD0 - Stablecoin (FUSDEV's underlying asset)
access(all) let pyusd0Address = "0x99aF3EeA856556646C98c8B9b2548Fe815240750"

// MOET - Flow Omni Token
access(all) let moetAddress = "0x213979bB8A9A86966999b3AA797C1fcf3B967ae2"

// WFLOW - Wrapped Flow
access(all) let wflowAddress = "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"

// ============================================================================
// STORAGE SLOT CONSTANTS
// ============================================================================

// Token balanceOf mapping slots (for EVM.store to manipulate balances)
access(all) let moetBalanceSlot = 0 as UInt256
access(all) let pyusd0BalanceSlot = 1 as UInt256
access(all) let fusdevBalanceSlot = 12 as UInt256
access(all) let wflowBalanceSlot = 1 as UInt256

// Morpho vault storage slots
access(all) let morphoVaultTotalSupplySlot = 11 as UInt256
access(all) let morphoVaultTotalAssetsSlot = 15 as UInt256

// ============================================================================
// FEE COMPENSATING CONSTANTS
// ============================================================================

// helps match expected values by increasing the amount of tokens we would get
// normally amount of tokens we would get is true_price * (1 - fee_rate)
// now we get true_price / (1 - fee_rate) * (1 - fee_rate) = true_price
access(all) let fee3000Premium: UFix64 = 1.0 / (1.0-0.003)
access(all) let fee100Premium: UFix64 = 1.0 / (1.0 - 0.0001)
Comment on lines +73 to +77

Choose a reason for hiding this comment

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

👌 This makes sense to me, we should be able to get our numbers much, much closer like this.

The only thing we need to be mindful of is that this adjustment and swaps only happen in a single direction. Conceptually, I would think that this should be the case (why swap both ways & pay extra fees if you never needed to swap the tokens in the first place), but just wanted to flag this if we start to see any irreducible discrepancies.

The other option is that we could just tighten our assertions by calculating fee adjusted values to test against.


access(all)
fun setup() {
// Deploy all contracts for mainnet fork
deployContractsForFork()

// Setup Uniswap V3 pools with structurally valid state
// This sets slot0, observations, liquidity, ticks, bitmap, positions, and POOL token balances
setPoolToPrice(
factoryAddress: factoryAddress,
tokenAAddress: pyusd0Address,
tokenBAddress: morphoVaultAddress,
fee: 100,
priceTokenBPerTokenA: fee100Premium,
tokenABalanceSlot: pyusd0BalanceSlot,
tokenBBalanceSlot: fusdevBalanceSlot,
signer: coaOwnerAccount
)

setPoolToPrice(
factoryAddress: factoryAddress,
tokenAAddress: pyusd0Address,
tokenBAddress: wflowAddress,
fee: 3000,
priceTokenBPerTokenA: fee3000Premium,
tokenABalanceSlot: pyusd0BalanceSlot,
tokenBBalanceSlot: wflowBalanceSlot,
signer: coaOwnerAccount
)

setPoolToPrice(
factoryAddress: factoryAddress,
tokenAAddress: moetAddress,
tokenBAddress: morphoVaultAddress,
fee: 100,
priceTokenBPerTokenA: fee100Premium,
tokenABalanceSlot: moetBalanceSlot,
tokenBBalanceSlot: fusdevBalanceSlot,
signer: coaOwnerAccount
)

setPoolToPrice(
factoryAddress: factoryAddress,
tokenAAddress: moetAddress,
tokenBAddress: pyusd0Address,
fee: 100,
priceTokenBPerTokenA: fee100Premium,
tokenABalanceSlot: moetBalanceSlot,
tokenBBalanceSlot: pyusd0BalanceSlot,
signer: coaOwnerAccount
)

// BandOracle is only used for FLOW price for FlowALP collateral
let symbolPrices: {String: UFix64} = {
"FLOW": 1.0,
"USD": 1.0
}
setBandOraclePrices(signer: bandOracleAccount, symbolPrices: symbolPrices)

let reserveAmount = 100_000_00.0
transferFlow(signer: whaleFlowAccount, recipient: flowALPAccount.address, amount: reserveAmount)
mintMoet(signer: flowALPAccount, to: flowALPAccount.address, amount: reserveAmount, beFailed: false)

// Fund FlowYieldVaults account for scheduling fees (atomic initial scheduling)
transferFlow(signer: whaleFlowAccount, recipient: flowYieldVaultsAccount.address, amount: 100.0)
}

access(all) var testSnapshot: UInt64 = 0
access(all)
fun test_ForkedRebalanceYieldVaultScenario1() {
let fundingAmount = 1000.0

let user = Test.createAccount()

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

// Expected values from Google sheet calculations
let expectedYieldTokenValues: {UFix64: UFix64} = {
0.5: 307.69230769,
Copy link
Contributor

@liobrasil liobrasil Jan 27, 2026

Choose a reason for hiding this comment

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

The expectedYieldTokenValues are the debt notional (at target health factor) in the unit of account i.e MOET/USD: expectedYieldTokenValue = expectedDebt = fundingAmount * flowPrice * collateralFactor / targetHealthFactor. It doesn't not account for the fees associated with swapping (nor price impact) MOET to yieldToken on the DEX when we create the yieldVault (ie. yieldToMoetSwapper in the createStrategy), as a consequence: expectedYieldTokenValue < expectedDebt.The bridge fees are in FLOW and covered by the protocol.

0.8: 492.30769231,
1.0: 615.38461538,
1.2: 738.46153846,
1.5: 923.07692308,
2.0: 1230.76923077,
3.0: 1846.15384615,
5.0: 3076.92307692
}

// Likely 0.0
let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!
transferFlow(signer: whaleFlowAccount, recipient: user.address, amount: fundingAmount)
grantBeta(flowYieldVaultsAccount, user)

// Set vault to baseline 1:1 price
// Use 1 billion (1e9) as base to prevent slippage, safe from UFix64 overflow
setVaultSharePrice(
vaultAddress: morphoVaultAddress,
assetAddress: pyusd0Address,
assetBalanceSlot: pyusd0BalanceSlot,
totalSupplySlot: morphoVaultTotalSupplySlot,
vaultTotalAssetsSlot: morphoVaultTotalAssetsSlot,
baseAssets: 1000000000.0,
priceMultiplier: 1.0,
signer: user
)

createYieldVault(
signer: user,
strategyIdentifier: strategyIdentifier,
vaultIdentifier: flowTokenIdentifier,
amount: fundingAmount,
beFailed: false
)

// Capture the actual position ID from the FlowCreditMarket.Opened event
var pid = (getLastPositionOpenedEvent(Test.eventsOfType(Type<FlowALPv0.Opened>())) as! FlowALPv0.Opened).pid
log("[TEST] Captured Position ID from event: \(pid)")

var yieldVaultIDs = getYieldVaultIDs(address: user.address)
log("[TEST] YieldVault ID: \(yieldVaultIDs![0])")
Test.assert(yieldVaultIDs != nil, message: "Expected user's YieldVault IDs to be non-nil but encountered nil")
Test.assertEqual(1, yieldVaultIDs!.length)

var yieldVaultBalance = getYieldVaultBalance(address: user.address, yieldVaultID: yieldVaultIDs![0])

log("[TEST] Initial yield vault balance: \(yieldVaultBalance ?? 0.0)")

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

testSnapshot = getCurrentBlockHeight()

for flowPrice in flowPrices {
if (getCurrentBlockHeight() > testSnapshot) {
Test.reset(to: testSnapshot)
}
yieldVaultBalance = getYieldVaultBalance(address: user.address, yieldVaultID: yieldVaultIDs![0])

log("[TEST] YieldVault balance before flow price \(flowPrice) \(yieldVaultBalance ?? 0.0)")

// === FLOW PRICE CHANGES ===
setBandOraclePrices(signer: bandOracleAccount, symbolPrices: {
"FLOW": flowPrice,
"USD": 1.0
})

// Update PYUSD0/FLOW pool to match new Flow price
// priceTokenBPerTokenA = how many tokens of tokenB we get for 1 token of tokenA
// if flow price = 2.0 then priceTokenBPerTokenA = 1.0 / 2.0 = 0.5
setPoolToPrice(
factoryAddress: factoryAddress,
tokenAAddress: pyusd0Address,
tokenBAddress: wflowAddress,
fee: 3000,
priceTokenBPerTokenA: fee3000Premium / flowPrice,
tokenABalanceSlot: pyusd0BalanceSlot,
tokenBBalanceSlot: wflowBalanceSlot,
signer: coaOwnerAccount
)

yieldVaultBalance = getYieldVaultBalance(address: user.address, yieldVaultID: yieldVaultIDs![0])

log("[TEST] YieldVault balance before flow price \(flowPrice) rebalance: \(yieldVaultBalance ?? 0.0)")

// Get yield token balance before rebalance
let yieldTokensBefore = getAutoBalancerBalance(id: yieldVaultIDs![0]) ?? 0.0
let currentValueBefore = getAutoBalancerCurrentValue(id: yieldVaultIDs![0]) ?? 0.0

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

yieldVaultBalance = getYieldVaultBalance(address: user.address, yieldVaultID: yieldVaultIDs![0])

log("[TEST] YieldVault balance after flow before \(flowPrice): \(yieldVaultBalance ?? 0.0)")

// Get yield token balance after rebalance
let yieldTokensAfter = getAutoBalancerBalance(id: yieldVaultIDs![0]) ?? 0.0
let currentValueAfter = getAutoBalancerCurrentValue(id: yieldVaultIDs![0]) ?? 0.0

// Get expected yield tokens from Google sheet calculations
let expectedYieldTokens = expectedYieldTokenValues[flowPrice] ?? 0.0

log("\n=== SCENARIO 1 DETAILS for Flow Price \(flowPrice) ===")
log("YieldVault Balance: \(yieldVaultBalance ?? 0.0)")
log("Yield Tokens Before: \(yieldTokensBefore)")
log("Yield Tokens After: \(yieldTokensAfter)")
log("Expected Yield Tokens: \(expectedYieldTokens)")
let precisionDiff = yieldTokensAfter > expectedYieldTokens ? yieldTokensAfter - expectedYieldTokens : expectedYieldTokens - yieldTokensAfter
let precisionSign = yieldTokensAfter > expectedYieldTokens ? "+" : "-"
log("Precision Difference: \(precisionSign)\(precisionDiff)")
let percentDiff = expectedYieldTokens > 0.0 ? (precisionDiff / expectedYieldTokens) * 100.0 : 0.0
log("Percent Difference: \(precisionSign)\(percentDiff)%")

// check if percent difference is within tolerance
let percentToleranceCheck = equalAmounts(a: percentDiff, b: 0.0, tolerance: forkedPercentTolerance)
Test.assert(percentToleranceCheck, message: "Percent difference \(percentDiff)% is not within tolerance \(forkedPercentTolerance)%")

let yieldChange = yieldTokensAfter > yieldTokensBefore ? yieldTokensAfter - yieldTokensBefore : yieldTokensBefore - yieldTokensAfter
let yieldSign = yieldTokensAfter > yieldTokensBefore ? "+" : "-"
log("Yield Token Change: \(yieldSign)\(yieldChange)")
log("Current Value Before: \(currentValueBefore)")
log("Current Value After: \(currentValueAfter)")
let valueChange = currentValueAfter > currentValueBefore ? currentValueAfter - currentValueBefore : currentValueBefore - currentValueAfter
let valueSign = currentValueAfter > currentValueBefore ? "+" : "-"
log("Value Change: \(valueSign)\(valueChange)")
log("=============================================\n")
}

// closeYieldVault(signer: user, id: yieldVaultIDs![0], beFailed: false)

let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!
log("[TEST] flow balance after \(flowBalanceAfter)")
}
Loading