diff --git a/cadence/tests/forked_rebalance_scenario3d_test.cdc b/cadence/tests/forked_rebalance_scenario3d_test.cdc new file mode 100644 index 00000000..e4329063 --- /dev/null +++ b/cadence/tests/forked_rebalance_scenario3d_test.cdc @@ -0,0 +1,406 @@ +#test_fork(network: "mainnet-fork", height: 143292255) + +import Test +import BlockchainHelpers + +import "test_helpers.cdc" +import "evm_state_helpers.cdc" + +import "FlowYieldVaults" +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 ALP USD +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 = 3 as UInt256 + +// Morpho vault storage slots +access(all) let morphoVaultTotalSupplySlot = 11 as UInt256 +access(all) let morphoVaultTotalAssetsSlot = 15 as UInt256 + +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: feeAdjustedPrice(1.0, fee: 100, reverse: false), + tokenABalanceSlot: pyusd0BalanceSlot, + tokenBBalanceSlot: fusdevBalanceSlot, + signer: coaOwnerAccount + ) + + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: pyusd0Address, + tokenBAddress: wflowAddress, + fee: 3000, + priceTokenBPerTokenA: feeAdjustedPrice(1.0, fee: 3000, reverse: false), + tokenABalanceSlot: pyusd0BalanceSlot, + tokenBBalanceSlot: wflowBalanceSlot, + signer: coaOwnerAccount + ) + + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: moetAddress, + tokenBAddress: morphoVaultAddress, + fee: 100, + priceTokenBPerTokenA: feeAdjustedPrice(1.0, fee: 100, reverse: false), + tokenABalanceSlot: moetBalanceSlot, + tokenBBalanceSlot: fusdevBalanceSlot, + signer: coaOwnerAccount + ) + + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: moetAddress, + tokenBAddress: pyusd0Address, + fee: 100, + priceTokenBPerTokenA: feeAdjustedPrice(1.0, fee: 100, reverse: false), + 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_ForkedRebalanceYieldVaultScenario3D() { + let fundingAmount = 1000.0 + let flowPriceDecrease = 0.5 + let yieldPriceIncrease = 1.5 + + // Expected values from Google sheet calculations + let expectedYieldTokenValues = [615.38461539, 307.69230769, 268.24457594] + let expectedFlowCollateralValues = [1000.0, 500.0, 653.84615385] + let expectedDebtValues = [615.38461539, 307.69230769, 402.36686391] + + let user = Test.createAccount() + + // 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 + setVaultSharePrice( + vaultAddress: morphoVaultAddress, + assetAddress: pyusd0Address, + assetBalanceSlot: pyusd0BalanceSlot, + totalSupplySlot: morphoVaultTotalSupplySlot, + vaultTotalAssetsSlot: morphoVaultTotalAssetsSlot, + 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())) 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) + + let yieldTokensBefore = getAutoBalancerBalance(id: yieldVaultIDs![0])! + let debtBefore = getMOETDebtFromPosition(pid: pid) + let flowCollateralBefore = getFlowCollateralFromPosition(pid: pid) + let flowCollateralValueBefore = flowCollateralBefore * 1.0 + + log("\n=== PRECISION COMPARISON (Initial State) ===") + log("Expected Yield Tokens: \(expectedYieldTokenValues[0])") + log("Actual Yield Tokens: \(yieldTokensBefore)") + let diff0 = yieldTokensBefore > expectedYieldTokenValues[0] ? yieldTokensBefore - expectedYieldTokenValues[0] : expectedYieldTokenValues[0] - yieldTokensBefore + let sign0 = yieldTokensBefore > expectedYieldTokenValues[0] ? "+" : "-" + log("Difference: \(sign0)\(diff0)") + log("") + log("Expected Flow Collateral Value: \(expectedFlowCollateralValues[0])") + log("Actual Flow Collateral Value: \(flowCollateralValueBefore)") + let flowDiff0 = flowCollateralValueBefore > expectedFlowCollateralValues[0] ? flowCollateralValueBefore - expectedFlowCollateralValues[0] : expectedFlowCollateralValues[0] - flowCollateralValueBefore + let flowSign0 = flowCollateralValueBefore > expectedFlowCollateralValues[0] ? "+" : "-" + log("Difference: \(flowSign0)\(flowDiff0)") + log("") + log("Expected MOET Debt: \(expectedDebtValues[0])") + log("Actual MOET Debt: \(debtBefore)") + let debtDiff0 = debtBefore > expectedDebtValues[0] ? debtBefore - expectedDebtValues[0] : expectedDebtValues[0] - debtBefore + let debtSign0 = debtBefore > expectedDebtValues[0] ? "+" : "-" + log("Difference: \(debtSign0)\(debtDiff0)") + log("=========================================================\n") + + Test.assert( + equalAmounts(a: yieldTokensBefore, b: expectedYieldTokenValues[0], tolerance: 0.01), + message: "Expected yield tokens to be \(expectedYieldTokenValues[0]) but got \(yieldTokensBefore)" + ) + Test.assert( + equalAmounts(a: flowCollateralValueBefore, b: expectedFlowCollateralValues[0], tolerance: 0.01), + message: "Expected flow collateral value to be \(expectedFlowCollateralValues[0]) but got \(flowCollateralValueBefore)" + ) + Test.assert( + equalAmounts(a: debtBefore, b: expectedDebtValues[0], tolerance: 0.01), + message: "Expected MOET debt to be \(expectedDebtValues[0]) but got \(debtBefore)" + ) + + // === FLOW PRICE DECREASE TO 0.5 === + log("\n=== FLOW PRICE → 0.5x ===") + setBandOraclePrices(signer: bandOracleAccount, symbolPrices: { + "FLOW": flowPriceDecrease, + "USD": 1.0 + }) + + // FLOW=$0.5, so 1 WFLOW = flowPriceDecrease PYUSD0 + // Undercollat sells FUSDEV→PYUSD0→WFLOW; last hop is PYUSD0→WFLOW (reverse on this pool) + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: wflowAddress, + tokenBAddress: pyusd0Address, + fee: 3000, + priceTokenBPerTokenA: feeAdjustedPrice(UFix128(flowPriceDecrease), fee: 3000, reverse: true), + tokenABalanceSlot: wflowBalanceSlot, + tokenBBalanceSlot: pyusd0BalanceSlot, + signer: coaOwnerAccount + ) + + // MOET/FUSDEV pool: fee adjustment depends on rebalance type + // Deficit (flowPrice < 1.0): swaps FUSDEV→MOET (reverse on this pool) + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: moetAddress, + tokenBAddress: morphoVaultAddress, + fee: 100, + priceTokenBPerTokenA: feeAdjustedPrice(1.0, fee: 100, reverse: true), + tokenABalanceSlot: moetBalanceSlot, + tokenBBalanceSlot: fusdevBalanceSlot, + signer: coaOwnerAccount + ) + + rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) + rebalancePosition(signer: flowALPAccount, pid: pid, force: true, beFailed: false) + + let yieldTokensAfterFlowPriceDecrease = getAutoBalancerBalance(id: yieldVaultIDs![0])! + let flowCollateralAfterFlowDecrease = getFlowCollateralFromPosition(pid: pid) + let flowCollateralValueAfterFlowDecrease = flowCollateralAfterFlowDecrease * flowPriceDecrease + let debtAfterFlowDecrease = getMOETDebtFromPosition(pid: pid) + + log("\n=== PRECISION COMPARISON (After Flow Price Decrease) ===") + log("Expected Yield Tokens: \(expectedYieldTokenValues[1])") + log("Actual Yield Tokens: \(yieldTokensAfterFlowPriceDecrease)") + let diff1 = yieldTokensAfterFlowPriceDecrease > expectedYieldTokenValues[1] ? yieldTokensAfterFlowPriceDecrease - expectedYieldTokenValues[1] : expectedYieldTokenValues[1] - yieldTokensAfterFlowPriceDecrease + let sign1 = yieldTokensAfterFlowPriceDecrease > expectedYieldTokenValues[1] ? "+" : "-" + log("Difference: \(sign1)\(diff1)") + log("") + log("Expected Flow Collateral Value: \(expectedFlowCollateralValues[1])") + log("Actual Flow Collateral Value: \(flowCollateralValueAfterFlowDecrease)") + log("Actual Flow Collateral Amount: \(flowCollateralAfterFlowDecrease) Flow tokens") + let flowDiff1 = flowCollateralValueAfterFlowDecrease > expectedFlowCollateralValues[1] ? flowCollateralValueAfterFlowDecrease - expectedFlowCollateralValues[1] : expectedFlowCollateralValues[1] - flowCollateralValueAfterFlowDecrease + let flowSign1 = flowCollateralValueAfterFlowDecrease > expectedFlowCollateralValues[1] ? "+" : "-" + log("Difference: \(flowSign1)\(flowDiff1)") + log("") + log("Expected MOET Debt: \(expectedDebtValues[1])") + log("Actual MOET Debt: \(debtAfterFlowDecrease)") + let debtDiff1 = debtAfterFlowDecrease > expectedDebtValues[1] ? debtAfterFlowDecrease - expectedDebtValues[1] : expectedDebtValues[1] - debtAfterFlowDecrease + let debtSign1 = debtAfterFlowDecrease > expectedDebtValues[1] ? "+" : "-" + log("Difference: \(debtSign1)\(debtDiff1)") + log("=========================================================\n") + + Test.assert( + equalAmounts(a: yieldTokensAfterFlowPriceDecrease, b: expectedYieldTokenValues[1], tolerance: 0.01), + message: "Expected yield tokens after flow price decrease to be \(expectedYieldTokenValues[1]) but got \(yieldTokensAfterFlowPriceDecrease)" + ) + Test.assert( + equalAmounts(a: flowCollateralValueAfterFlowDecrease, b: expectedFlowCollateralValues[1], tolerance: 0.01), + message: "Expected flow collateral value after flow price decrease to be \(expectedFlowCollateralValues[1]) but got \(flowCollateralValueAfterFlowDecrease)" + ) + Test.assert( + equalAmounts(a: debtAfterFlowDecrease, b: expectedDebtValues[1], tolerance: 0.01), + message: "Expected MOET debt after flow price decrease to be \(expectedDebtValues[1]) but got \(debtAfterFlowDecrease)" + ) + + // === YIELD VAULT PRICE INCREASE TO 1.5 === + log("\n=== YIELD VAULT PRICE → 1.5x ===") + + setVaultSharePrice( + vaultAddress: morphoVaultAddress, + assetAddress: pyusd0Address, + assetBalanceSlot: pyusd0BalanceSlot, + totalSupplySlot: morphoVaultTotalSupplySlot, + vaultTotalAssetsSlot: morphoVaultTotalAssetsSlot, + priceMultiplier: yieldPriceIncrease, + signer: user + ) + + // FUSDEV is now worth 1.5x: 1 FUSDEV = yieldPriceIncrease PYUSD0 + // Surplus swaps FUSDEV→PYUSD0 (forward on this pool) + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: morphoVaultAddress, + tokenBAddress: pyusd0Address, + fee: 100, + priceTokenBPerTokenA: feeAdjustedPrice(UFix128(yieldPriceIncrease), fee: 100, reverse: false), + tokenABalanceSlot: fusdevBalanceSlot, + tokenBBalanceSlot: pyusd0BalanceSlot, + signer: coaOwnerAccount + ) + + // 1 FUSDEV = yieldPriceIncrease MOET (FUSDEV is now worth 1.5x) + // Overcollat swaps MOET→FUSDEV (reverse on this pool) + setPoolToPrice( + factoryAddress: factoryAddress, + tokenAAddress: morphoVaultAddress, + tokenBAddress: moetAddress, + fee: 100, + priceTokenBPerTokenA: feeAdjustedPrice(UFix128(yieldPriceIncrease), fee: 100, reverse: true), + tokenABalanceSlot: fusdevBalanceSlot, + tokenBBalanceSlot: moetBalanceSlot, + signer: coaOwnerAccount + ) + + rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) + + let yieldTokensAfterYieldPriceIncrease = getAutoBalancerBalance(id: yieldVaultIDs![0])! + let flowCollateralAfterYieldIncrease = getFlowCollateralFromPosition(pid: pid) + let flowCollateralValueAfterYieldIncrease = flowCollateralAfterYieldIncrease * flowPriceDecrease // Flow price remains at 0.5 + let debtAfterYieldIncrease = getMOETDebtFromPosition(pid: pid) + + log("\n=== PRECISION COMPARISON (After Yield Price Increase) ===") + log("Expected Yield Tokens: \(expectedYieldTokenValues[2])") + log("Actual Yield Tokens: \(yieldTokensAfterYieldPriceIncrease)") + let diff2 = yieldTokensAfterYieldPriceIncrease > expectedYieldTokenValues[2] ? yieldTokensAfterYieldPriceIncrease - expectedYieldTokenValues[2] : expectedYieldTokenValues[2] - yieldTokensAfterYieldPriceIncrease + let sign2 = yieldTokensAfterYieldPriceIncrease > expectedYieldTokenValues[2] ? "+" : "-" + log("Difference: \(sign2)\(diff2)") + log("") + log("Expected Flow Collateral Value: \(expectedFlowCollateralValues[2])") + log("Actual Flow Collateral Value: \(flowCollateralValueAfterYieldIncrease)") + log("Actual Flow Collateral Amount: \(flowCollateralAfterYieldIncrease) Flow tokens") + let flowDiff2 = flowCollateralValueAfterYieldIncrease > expectedFlowCollateralValues[2] ? flowCollateralValueAfterYieldIncrease - expectedFlowCollateralValues[2] : expectedFlowCollateralValues[2] - flowCollateralValueAfterYieldIncrease + let flowSign2 = flowCollateralValueAfterYieldIncrease > expectedFlowCollateralValues[2] ? "+" : "-" + log("Difference: \(flowSign2)\(flowDiff2)") + log("") + log("Expected MOET Debt: \(expectedDebtValues[2])") + log("Actual MOET Debt: \(debtAfterYieldIncrease)") + let debtDiff2 = debtAfterYieldIncrease > expectedDebtValues[2] ? debtAfterYieldIncrease - expectedDebtValues[2] : expectedDebtValues[2] - debtAfterYieldIncrease + let debtSign2 = debtAfterYieldIncrease > expectedDebtValues[2] ? "+" : "-" + log("Difference: \(debtSign2)\(debtDiff2)") + log("=========================================================\n") + + Test.assert( + equalAmounts(a: yieldTokensAfterYieldPriceIncrease, b: expectedYieldTokenValues[2], tolerance: 0.01), + message: "Expected yield tokens after yield price increase to be \(expectedYieldTokenValues[2]) but got \(yieldTokensAfterYieldPriceIncrease)" + ) + Test.assert( + equalAmounts(a: flowCollateralValueAfterYieldIncrease, b: expectedFlowCollateralValues[2], tolerance: 0.01), + message: "Expected flow collateral value after yield price increase to be \(expectedFlowCollateralValues[2]) but got \(flowCollateralValueAfterYieldIncrease)" + ) + Test.assert( + equalAmounts(a: debtAfterYieldIncrease, b: expectedDebtValues[2], tolerance: 0.01), + message: "Expected MOET debt after yield price increase to be \(expectedDebtValues[2]) but got \(debtAfterYieldIncrease)" + ) + + // TODO: closeYieldVault currently fails due to precision issues + // closeYieldVault(signer: user, id: yieldVaultIDs![0], beFailed: false) + + log("\n=== TEST COMPLETE ===") +} + + +// Helper function to get Flow collateral from position +access(all) fun getFlowCollateralFromPosition(pid: UInt64): UFix64 { + let positionDetails = getPositionDetails(pid: pid, beFailed: false) + for balance in positionDetails.balances { + if balance.vaultType == Type<@FlowToken.Vault>() { + if balance.direction == FlowALPv0.BalanceDirection.Credit { + return balance.balance + } + } + } + return 0.0 +} + + +// Helper function to get MOET debt from position +access(all) fun getMOETDebtFromPosition(pid: UInt64): UFix64 { + let positionDetails = getPositionDetails(pid: pid, beFailed: false) + for balance in positionDetails.balances { + if balance.vaultType == Type<@MOET.Vault>() { + if balance.direction == FlowALPv0.BalanceDirection.Debit { + return balance.balance + } + } + } + return 0.0 +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index b494501c..cc5567ac 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -692,6 +692,10 @@ fun equalAmounts(a: UFix64, b: UFix64, tolerance: UFix64): Bool { /// access(all) fun setBandOraclePrices(signer: Test.TestAccount, symbolPrices: {String: UFix64}) { + // Move time by 1 second to ensure that the resolve time is in the future + // This prevents race conditions between consecutive calls to setBandOraclePrices + Test.moveTime(by: 1.0) + let symbolsRates: {String: UInt64} = {} for symbol in symbolPrices.keys { // BandOracle uses 1e9 multiplier for prices