Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
187 changes: 187 additions & 0 deletions cadence/tests/forked_rebalance_scenario1_test.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// this height guarantees enough liquidity for the test
#test_fork(network: "mainnet", height: 140164761)

import Test
import BlockchainHelpers

import "test_helpers.cdc"

// standards
import "EVM"
// FlowYieldVaults platform
import "FlowYieldVaults"
// vm bridge
import "FlowEVMBridgeConfig"
// live oracles
import "ERC4626PriceOracles"
// mocks
import "MockOracle"
import "MockSwapper"
// other
import "FlowToken"
import "MOET"
import "YieldToken"
import "FlowYieldVaultsStrategiesV1_1"
import "FlowCreditMarket"


// check (and update) flow.json for correct addresses
// mainnet addresses
access(all) let flowYieldVaultsAccount = Test.getAccount(0xb1d63873c3cc9f79)
access(all) let yieldTokenAccount = Test.getAccount(0xb1d63873c3cc9f79)
access(all) let flowCreditMarketAccount = Test.getAccount(0x6b00ff876c299c61)
access(all) let bandOracleAccount = Test.getAccount(0x6801a6222ebf784a)
access(all) let whaleFlowAccount = Test.getAccount(0x92674150c9213fc9)

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

access(all) let collateralFactor = 0.8
access(all) let targetHealthFactor = 1.3

access(all) var snapshot: UInt64 = 0

access(all)
fun setup() {
// BandOracle is only used for FLOW price for FCM collateral
let symbolPrices: {String: UFix64} = {
"FLOW": 1.0
}
setBandOraclePrices(signer: bandOracleAccount, symbolPrices: symbolPrices)

let reserveAmount = 100_000_00.0
// service account does not have enough flow to "mint"
// var mintFlowResult = mintFlow(to: flowCreditMarketAccount, amount: reserveAmount)
// Test.expect(mintFlowResult, Test.beSucceeded())
transferFlow(signer: whaleFlowAccount, recipient: flowCreditMarketAccount.address, amount: reserveAmount)

mintMoet(signer: flowCreditMarketAccount, to: flowCreditMarketAccount.address, amount: reserveAmount, beFailed: false)

// Fund FlowYieldVaults account for scheduling fees (atomic initial scheduling)
// service account does not have enough flow to "mint"
// mintFlowResult = mintFlow(to: flowYieldVaultsAccount, amount: 100.0)
// Test.expect(mintFlowResult, Test.beSucceeded())
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)!
// service account does not have enough flow to "mint"
// let mintFlowResult =The code snippet `mintFlow(to: user, amount: fundingAmount)` is a function call that mints a specified amount of a token (in this case, Flow tokens) to a specific user account.
// mintFlow(to: user, amount: fundingAmount)
// Test.expect(mintFlowResult, Test.beSucceeded())
transferFlow(signer: whaleFlowAccount, recipient: user.address, amount: fundingAmount)
grantBeta(flowYieldVaultsAccount, 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<FlowCreditMarket.Opened>())) as! FlowCreditMarket.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: flowCreditMarketAccount, 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)")

setBandOraclePrice(signer: bandOracleAccount, symbol: "FLOW", price: flowPrice)

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: flowCreditMarketAccount, 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)%")
log("Percent difference \(percentDiff)% is 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)")
}
57 changes: 57 additions & 0 deletions cadence/tests/test_helpers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import "FlowCreditMarket"
access(all) let serviceAccount = Test.serviceAccount()

/* --- Test execution helpers --- */
// tolerance for forked tests
access(all)
let forkedPercentTolerance = 0.05

access(all)
fun _executeScript(_ path: String, _ args: [AnyStruct]): Test.ScriptResult {
Expand Down Expand Up @@ -479,6 +482,16 @@ fun addSupportedTokenSimpleInterestCurve(
Test.expect(additionRes, Test.beSucceeded())
}

access(all)
fun setPoolMockOracle(signer: Test.TestAccount) {
let res = _executeTransaction(
"./transactions/set_mock_oracle.cdc",
[],
signer
)
Test.expect(res, Test.beSucceeded())
}

access(all)
fun rebalancePosition(signer: Test.TestAccount, pid: UInt64, force: Bool, beFailed: Bool) {
let rebalanceRes = _executeTransaction(
Expand Down Expand Up @@ -597,6 +610,50 @@ fun setMockSwapperLiquidityConnector(signer: Test.TestAccount, vaultStoragePath:
Test.expect(setRes, Test.beSucceeded())
}

/// Sets the BandOracle price for a given symbol (e.g., "FLOW", "USD")
/// The price is in USD, converted internally to BandOracle's 1e9 format
///
/// @param signer: The BandOracle admin account that has DataUpdater capability
/// @param symbol: The oracle symbol (e.g., "FLOW", "USD", "stFLOW")
/// @param price: The price in USD (e.g., 0.5 for $0.50, 1.0 for $1.00)
///
access(all)
fun setBandOraclePrice(signer: Test.TestAccount, symbol: String, price: UFix64) {
// BandOracle uses 1e9 multiplier for prices
// e.g., $1.00 = 1_000_000_000, $0.50 = 500_000_000
let priceAsUInt64 = UInt64(price * 1_000_000_000.0)
let symbolsRates: {String: UInt64} = { symbol: priceAsUInt64 }

let setRes = _executeTransaction(
"../../lib/FlowCreditMarket/FlowActions/cadence/tests/transactions/band-oracle/update_data.cdc",
[ symbolsRates ],
signer
)
Test.expect(setRes, Test.beSucceeded())
}

/// Sets multiple BandOracle prices at once
///
/// @param signer: The BandOracle admin account that has DataUpdater capability
/// @param symbolPrices: A dictionary mapping symbols to prices in USD
/// e.g., { "FLOW": 0.5, "USD": 1.0 }
///
access(all)
fun setBandOraclePrices(signer: Test.TestAccount, symbolPrices: {String: UFix64}) {
let symbolsRates: {String: UInt64} = {}
for symbol in symbolPrices.keys {
let price = symbolPrices[symbol]!
symbolsRates[symbol] = UInt64(price * 1_000_000_000.0)
}

let setRes = _executeTransaction(
"../../lib/FlowCreditMarket/FlowActions/cadence/tests/transactions/band-oracle/update_data.cdc",
[ symbolsRates ],
signer
)
Test.expect(setRes, Test.beSucceeded())
}

access(all)
fun equalAmounts(a: UFix64, b: UFix64, tolerance: UFix64): Bool {
if a > b {
Expand Down
24 changes: 24 additions & 0 deletions cadence/tests/transactions/set_mock_oracle.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import "FlowCreditMarket"
import "MockOracle"
import "DeFiActions"

/// Updates the pool's price oracle to use MockOracle
/// This is useful for testing purposes where we want to control token prices
///
transaction() {
let pool: auth(FlowCreditMarket.EGovernance) &FlowCreditMarket.Pool
let oracle: {DeFiActions.PriceOracle}

prepare(signer: auth(BorrowValue) &Account) {
self.pool = signer.storage.borrow<auth(FlowCreditMarket.EGovernance) &FlowCreditMarket.Pool>(from: FlowCreditMarket.PoolStoragePath)
?? panic("Could not borrow reference to Pool from \(FlowCreditMarket.PoolStoragePath) - ensure a Pool has been configured")

// Create a MockOracle.PriceOracle - the unitOfAccount will be set based on the pool's default token
self.oracle = MockOracle.PriceOracle()
}

execute {
self.pool.setPriceOracle(self.oracle)
}
}

Loading