diff --git a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc index 3ca84da8..49a7c478 100644 --- a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc +++ b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc @@ -119,10 +119,13 @@ fun testRecursiveWithdrawSource() { // // In this test, the topUpSource behavior is adversarial: it attempts to re-enter // the pool during the pull/deposit flow. We expect the transaction to fail. - let withdrawRes = executeTransaction( - "./transactions/flow-alp/pool-management/withdraw_from_position.cdc", - [positionID, flowTokenIdentifier, 1500.0, true], // pullFromTopUpSource: true - userAccount + let withdrawRes = withdrawFromPosition( + signer: userAccount, + positionId: positionID, + tokenTypeIdentifier: flowTokenIdentifier, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 1500.0, + pullFromTopUpSource: true ) Test.expect(withdrawRes, Test.beFailed()) diff --git a/cadence/tests/adversarial_type_spoofing_test.cdc b/cadence/tests/adversarial_type_spoofing_test.cdc index 1edb2614..863fa56e 100644 --- a/cadence/tests/adversarial_type_spoofing_test.cdc +++ b/cadence/tests/adversarial_type_spoofing_test.cdc @@ -58,10 +58,13 @@ fun testMaliciousSource() { Test.expect(openRes, Test.beSucceeded()) // withdraw 1337 Flow from the position - let withdrawRes = executeTransaction( - "./transactions/flow-alp/pool-management/withdraw_from_position.cdc", - [1 as UInt64, flowTokenIdentifier, 1337.0, true], - hackerAccount + let withdrawRes = withdrawFromPosition( + signer: hackerAccount, + positionId: 1, + tokenTypeIdentifier: flowTokenIdentifier, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 1337.0, + pullFromTopUpSource: true ) Test.expect(withdrawRes, Test.beFailed()) diff --git a/cadence/tests/fork_interest_rate_test.cdc b/cadence/tests/fork_interest_rate_test.cdc new file mode 100644 index 00000000..59be7b65 --- /dev/null +++ b/cadence/tests/fork_interest_rate_test.cdc @@ -0,0 +1,620 @@ +#test_fork(network: "mainnet-fork", height: 142528994) + +import Test +import BlockchainHelpers + +import "FlowToken" +import "FungibleToken" +import "MOET" +import "FlowALPEvents" + +import "test_helpers.cdc" + +access(all) let MAINNET_PROTOCOL_ACCOUNT = Test.getAccount(MAINNET_PROTOCOL_ACCOUNT_ADDRESS) +access(all) let MAINNET_USDF_HOLDER = Test.getAccount(MAINNET_USDF_HOLDER_ADDRESS) +access(all) let MAINNET_WETH_HOLDER = Test.getAccount(MAINNET_WETH_HOLDER_ADDRESS) +access(all) let MAINNET_WBTC_HOLDER = Test.getAccount(MAINNET_WBTC_HOLDER_ADDRESS) +access(all) let MAINNET_FLOW_HOLDER = Test.getAccount(MAINNET_FLOW_HOLDER_ADDRESS) + +access(all) var snapshot: UInt64 = 0 + +// KinkCurve parameters (Aave v3 Volatile One) +access(all) let flowOptimalUtilization: UFix128 = 0.45 // 45% kink point +access(all) let flowBaseRate: UFix128 = 0.0 // 0% base rate +access(all) let flowSlope1: UFix128 = 0.04 // 4% slope below kink +access(all) let flowSlope2: UFix128 = 3.0 // 300% slope above kink + +// Fixed rate for MOET +access(all) let moetFixedRate: UFix128 = 0.04 + +access(all) +fun safeReset() { + let cur = getCurrentBlockHeight() + if cur > snapshot { + Test.reset(to: snapshot) + } +} + +access(all) +fun setup() { + deployContracts() + + createAndStorePool(signer: MAINNET_PROTOCOL_ACCOUNT, defaultTokenIdentifier: MAINNET_MOET_TOKEN_ID, beFailed: false) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0) + + addSupportedTokenKinkCurve( + signer: MAINNET_PROTOCOL_ACCOUNT, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + collateralFactor: 0.8, + borrowFactor: 0.9, + optimalUtilization: flowOptimalUtilization, + baseRate: flowBaseRate, + slope1: flowSlope1, + slope2: flowSlope2, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // set MOET to use a FixedRateInterestCurve at 4% APY. + setInterestCurveFixed( + signer: MAINNET_PROTOCOL_ACCOUNT, + tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, + yearlyRate: moetFixedRate + ) + + let res = setInsuranceSwapper( + signer: MAINNET_PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + swapperOutTypeIdentifier: MAINNET_MOET_TOKEN_ID, + priceRatio: 1.0, + ) + Test.expect(res, Test.beSucceeded()) + + let setInsRes = setInsuranceRate( + signer: MAINNET_PROTOCOL_ACCOUNT, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + insuranceRate: 0.001, + ) + Test.expect(setInsRes, Test.beSucceeded()) + + snapshot = getCurrentBlockHeight() +} + +// ============================================================================= +/// Verifies protocol behavior when extreme utilization (nearly all liquidity borrowed) +// ============================================================================= +access(all) +fun test_extreme_utilization() { + safeReset() + + setInterestCurveKink( + signer: MAINNET_PROTOCOL_ACCOUNT, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + optimalUtilization: flowOptimalUtilization, + baseRate: flowBaseRate, + slope1: flowSlope1, + slope2: flowSlope2 + ) + + // create Flow LP with 2000 FLOW + let FLOWAmount = 2000.0 + + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_FLOW_HOLDER, amount: FLOWAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + var openEvents = Test.eventsOfType(Type()) + let lpDepositPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + + // create borrower with MOET collateral + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 10_000.0, beFailed: false) + + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 10_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + openEvents = Test.eventsOfType(Type()) + let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + + // borrow 1800 FLOW (90% of 2000 FLOW credit) + borrowFromPosition( + signer: borrower, + positionId: borrowerPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 1800.0, + beFailed: false + ) + + // Pool state: + // FLOW credit = 2000 + // FLOW debit = 1800 + // + // KinkInterestCurve: + // utilization = debitBalance / creditBalance + // utilization = 1800 / 2000 = 0.9 = 90% > 45% (above kink) + // + // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) + // excessUtilization = (0.9 - 0.45) / (1 - 0.45) = 0.45 / 0.55 = 0.81818181818... + // + // rate = baseRate + slope1 + (slope2 * excessUtilization) + // rate = 0.0 + 0.04 + 3.0 * 0.81818181818 = 2.49454545454... (249.45% APY) + + // record initial state + let detailsBefore = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtBefore = getDebitBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) + + // advance 30 days + Test.moveTime(by: THIRTY_DAYS) + Test.commitBlock() + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // 30 days growth rate = perSecondRate ^ 2_592_000 - 1 + // FLOW debit 30 days growth rate = (1 + 2.49454545 / 31557600)^2592000 - 1 = 0.22739266 //0.22739266 + let expectedFLOWGrowthRate = 0.22739266 + + // verify debt growth + let detailsAfter = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtAfter = getDebitBalanceForType(details: detailsAfter, vaultType: Type<@FlowToken.Vault>()) + Test.assert(FLOWDebtAfter > FLOWDebtBefore, message: "Debt should increase at above-kink utilization") + + let FLOWDebtGrowth = FLOWDebtAfter - FLOWDebtBefore + let FLOWGrowthRate = FLOWDebtGrowth / FLOWDebtBefore + + // NOTE: TODO(Uliana): update to equalWithinVariance when PR https://github.com/onflow/FlowALP/pull/255 will be merged + // We intentionally do not use `equalWithinVariance` with `defaultUFixVariance` here. + // The default variance is designed for deterministic math, but insurance collection + // depends on block timestamps, which can differ slightly between test runs. + // A larger, time-aware tolerance is required. + let tolerance = 0.00001 + var diff = expectedFLOWGrowthRate > FLOWGrowthRate + ? expectedFLOWGrowthRate - FLOWGrowthRate + : FLOWGrowthRate - expectedFLOWGrowthRate + Test.assert(diff < tolerance, message: "Expected FLOW debt growth rate to be \(expectedFLOWGrowthRate) but got \(FLOWGrowthRate)") +} + +// ============================================================================= +/// Verifies protocol behavior when a lending pool has liquidity but no borrows. +// ============================================================================= +access(all) +fun test_zero_credit_balance() { + safeReset() + + // setup borrower, create MOET position + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + + let MOETAmount = 10_000.0 + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: MOETAmount, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: MOETAmount, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + // no Flow LP is created — pool has zero FLOW liquidity + + // attempt to borrow FLOW (no reserves) + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + + let borrowRes = _executeTransaction( + "./transactions/position-manager/borrow_from_position.cdc", + [pid, MAINNET_FLOW_TOKEN_ID, FLOW_VAULT_STORAGE_PATH, 100.0], + borrower + ) + Test.expect(borrowRes, Test.beFailed()) + + // FLOW interest rate calculation (KinkInterestCurve) + // + // totalCreditBalance = 0 + // totalDebitBalance = 0 + // baseRate = 0 + // + // debitRate: + // debitRate = (if no debt, debitRate = base rate) = 0 + // + // creditRate: + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) + // debitIncome = totalDebitBalance * debitRate + // + // debitIncome = 0.0 * 0.0 = 0.0 + // protocolFeeAmount = 0.0 + // totalCreditBalance = 0.0 -> creditRate = 0.0 + + // MOET interest rate calculation (FixedRateInterestCurve) + // + // totalCreditBalance = 10000 + // totalDebitBalance = 0 + // + // debitRate: + // debitRate = yearlyRate = 0.04 + // + // creditRate: + // creditRate = debitRate * (1.0 - protocolFeeRate) + // protocolFeeRate = insuranceRate + stabilityFeeRate + // + // protocolFeeRate = 0.001 * 0.05 = 0.051 + // creditRate = 0.04 * (1 - 0.051) = 0.03796 (3.796% APY) + + Test.moveTime(by: THIRTY_DAYS) + Test.commitBlock() + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // 30DaysGrowth = perSecondRate^THIRTY_DAYS - 1 + // + // FLOW debt 30 days growth = (1 + 0/31_557_600)^2_592_000 - 1 = 0 + // MOET credit 30 days growth = (1 + 0.03796/31_557_600)^2_592_000 - 1 = 0.0003122730069 + let detailsAfterTime = getPositionDetails(pid: pid, beFailed: false) + let moetCredit = getCreditBalanceForType(details: detailsAfterTime, vaultType: Type<@MOET.Vault>()) + Test.assert(moetCredit > 10000.0, message: "MOET credit should accrue interest") + + // add FLOW liquidity + let FLOWAmount = 5000.0 + let flowLp = Test.createAccount() + transferFungibleTokens( + tokenIdentifier: MAINNET_FLOW_TOKEN_ID, + from: MAINNET_FLOW_HOLDER, + to: flowLp, + amount: FLOWAmount + ) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: flowLp, amount: FLOWAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + // borrow FLOW (Flow LP deposited 5000.0 FLOW, liquidity now available) + borrowFromPosition( + signer: borrower, + positionId: pid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 100.0, + beFailed: false + ) + + let details = getPositionDetails(pid: pid, beFailed: false) + let flowDebt = getDebitBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) + Test.assertEqual(100.0, flowDebt) + + // FLOW interest rate calculation (KinkInterestCurve) + // + // totalCreditBalance = 5000 + // totalDebitBalance = 100 + // + // debitRate: + // utilization = debitBalance / creditBalance + // utilization = 100 / 5000 = 0.02 < 0.45 (below kink) + // + // debitRate = baseRate + (slope1 * utilization / optimalUtilization) + // debitRate = 0.0 + (0.04 * 0.02 / 0.45) = 0.00177777777 (0.177% APY) + // + // creditRate: + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) + // debitIncome = totalDebitBalance * debitRate + // + // debitIncome = 0.0 * 0.0 = 0.0 + // protocolFeeAmount = 0.0 + // totalCreditBalance = 0.0 -> creditRate = 0.0 + + // Advance 1 day to measure exact interest growth + Test.moveTime(by: DAY) + Test.commitBlock() + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // daily growth rate = perSecondRate^86400 - 1 + // FLOW debt daily growth rate = (1 + 0.00177777777 / 31_557_600)^86400 - 1 = 0.00000486766 + let expectedFlowDebtDailyGrowth = 0.00000486 + + let detailsAfter1Day = getPositionDetails(pid: pid, beFailed: false) + let flowDebtAfter1Day = getDebitBalanceForType(details: detailsAfter1Day, vaultType: Type<@FlowToken.Vault>()) + let flowDebtDailyGrowth = (flowDebtAfter1Day - flowDebt) / flowDebt + Test.assertEqual(expectedFlowDebtDailyGrowth, flowDebtDailyGrowth) +} + +// ============================================================================= +/// Verifies protocol behavior when a lending pool has liquidity but no borrows. +// ============================================================================= +access(all) +fun test_empty_pool() { + safeReset() + + // create Flow LP only — no borrowers + let flowLp = Test.createAccount() + let FLOWAmount = 10000.0 + transferFungibleTokens(tokenIdentifier: MAINNET_FLOW_TOKEN_ID, from: MAINNET_FLOW_HOLDER, to: flowLp, amount: FLOWAmount) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: flowLp, amount: FLOWAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let lpPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + + // record initial credit + let detailsBefore = getPositionDetails(pid: lpPid, beFailed: false) + let FLOWCreditBefore = getCreditBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) + + // advance 30 days with zero borrowing + Test.moveTime(by: THIRTY_DAYS) + Test.commitBlock() + + // FLOW rate calculation (KinkInterestCurve) + // baseRate:0 + // debitBalance:0 + // + // debitRate = (if no debt, debitRate = base rate) = 0 + // creditRate = 0 (debitIncome = 0) + let detailsAfterNoDebit = getPositionDetails(pid: lpPid, beFailed: false) + let FLOWCreditAfterNoDebit = getCreditBalanceForType(details: detailsAfterNoDebit, vaultType: Type<@FlowToken.Vault>()) + Test.assertEqual(FLOWCreditBefore, FLOWCreditAfterNoDebit) + + // create a borrower to trigger utilization + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 10_000.0, beFailed: false) + + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 10_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + let borrowerPid: UInt64 = 1 + borrowFromPosition( + signer: borrower, + positionId: borrowerPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 2_000.0, + beFailed: false + ) + + // advance another 30 days + Test.moveTime(by: THIRTY_DAYS) + Test.commitBlock() + + // KinkCurve + // utilization = debitBalance / creditBalance + // FLOW: 2000 / 10000 = 0.2 < 0.45 (below kink) + // + // debitRate = baseRate + (slope1 * utilization / optimalUtilization) + // FLOW: debitRate = 0 + 0.04 * (0.2 / 0.45) = 0.01777777777 (1.777% APY) + // + // creditRate: + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) + // debitIncome = totalDebitBalance * debitRate + // + // debitIncome = 2000 * 0.01777777777 = 35.55555554 + // protocolFeeAmount = 35.55555554 * (0.001 + 0.05) = 1.81333333254 + // creditRate = (35.55555554 - 1.81333333254) / 10000 = 0.00337422222 (0.337% APY) + + let detailsAfterDebit = getPositionDetails(pid: lpPid, beFailed: false) + let FLOWCreditAfterDebit = getCreditBalanceForType(details: detailsAfterDebit, vaultType: Type<@FlowToken.Vault>()) + + let FLOWCreditGrowth = FLOWCreditAfterDebit - FLOWCreditAfterNoDebit + let FLOWCreditGrowthRate = FLOWCreditGrowth / FLOWCreditAfterNoDebit + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // 30 Days Growth = perSecondRate^THIRTY_DAYS - 1 + // FLOW credit 30 days growth = (1 + 0.00337422222 / 31_557_600)^2_592_000 - 1 = 0.00027718 + let expectedFLOWCreditGrowthRate = 0.00027718 + Test.assertEqual(expectedFLOWCreditGrowthRate, FLOWCreditGrowthRate) +} + +// ============================================================================= +/// Verifies correct interest rate behavior at the utilization kink point. +// ============================================================================= +access(all) +fun test_kink_point_transition() { + safeReset() + + setInterestCurveKink( + signer: MAINNET_PROTOCOL_ACCOUNT, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + optimalUtilization: flowOptimalUtilization, + baseRate: flowBaseRate, + slope1: flowSlope1, + slope2: flowSlope2 + ) + + // create LP with 10000 FLOW + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_FLOW_HOLDER, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + // create borrower with large MOET collateral + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 100_000.0, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 100_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + + // KinkCurve + // To achieve exactly 45% utilization: + // utilization = debit / credit + // 0.45 = debit / 10000 + // debit = 10000 * 0.45 = 4500 + borrowFromPosition( + signer: borrower, + positionId: borrowerPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 4500.0, + beFailed: false + ) + + // KinkCurve + // utilization = debitBalance / creditBalance + // FLOW: 4500 / 10000 = 0.45 <= 0.45 (exactly at kink) + // + // debitRate = baseRate + (slope1 * utilization / optimalUtilization) + // FLOW: debitRate = 0.0 + (0.04 * 0.45 / 0.45) = 0.04 (4% APY) + + // record state at kink point + let detailsAtKink = getPositionDetails(pid: borrowerPid, beFailed: false) + let debtAtKink = getDebitBalanceForType(details: detailsAtKink, vaultType: Type<@FlowToken.Vault>()) + + // advance 1 year and verify rate matches 4% APY + Test.moveTime(by: ONE_YEAR) + Test.commitBlock() + + let detailsAfterYear = getPositionDetails(pid: borrowerPid, beFailed: false) + let debtAfterYear = getDebitBalanceForType(details: detailsAfterYear, vaultType: Type<@FlowToken.Vault>()) + let yearlyGrowthAtKink = (debtAfterYear - debtAtKink) / debtAtKink + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // yearly debt growth = perSecondRate^ONE_YEAR - 1 + // FLOW debit yearly growth = (1 + 0.04 / 31_557_600)^31_557_600 - 1 = 0.04081077417 (4.08%) + let expectedYearlyGrowthAtKink = 0.04081077 + Test.assertEqual(expectedYearlyGrowthAtKink, yearlyGrowthAtKink) +} + +// ============================================================================= +/// Verifies interest accrual over long time periods +// ============================================================================= +access(all) +fun test_long_time_period_accrual() { + safeReset() + + // create LP with 10000 FLOW + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_FLOW_HOLDER, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + var openEvents = Test.eventsOfType(Type()) + let lpPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + + // create borrower + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 100_000.0, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 100_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + openEvents = Test.eventsOfType(Type()) + let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + + // borrow 2000 FLOW + borrowFromPosition( + signer: borrower, + positionId: borrowerPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 2000.0, + beFailed: false + ) + + // Borrower FLOW rate calculation (KinkInterestCurve) + // KinkCurve + // utilization = debitBalance / creditBalance + // FLOW: 2000 / 10000 = 0.2 < 0.45 (below kink) + // + // debitRate = baseRate + (slope1 * utilization / optimalUtilization) + // FLOW: debitRate = 0 + 0.04 * (0.2 / 0.45) = 0.01777777777 (1.77% APY) + let expectedFLOWDebtRate = 0.01777777 + + let detailsBefore = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtBefore = getDebitBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) + + let FLOWCreditBefore = getCreditBalanceForType(details: getPositionDetails(pid: lpPid, beFailed: false), vaultType: Type<@FlowToken.Vault>()) + + // 1 full year + Test.moveTime(by: ONE_YEAR) + Test.commitBlock() + + let detailsAfter1Year = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtAfter1Year = getDebitBalanceForType(details: detailsAfter1Year, vaultType: Type<@FlowToken.Vault>()) + let FLOWGrowthRate1Year = (FLOWDebtAfter1Year - FLOWDebtBefore) / FLOWDebtBefore + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // yearly debt growth = perSecondRate^ONE_YEAR - 1 + // FLOW debit yearly growth = (1 + 0.017777778 / 31_557_600)^31_557_600 - 1 = 0.01793674 + let expectedFLOWDebtYearlyGrowth = 0.01793674 + Test.assertEqual(expectedFLOWDebtYearlyGrowth, FLOWGrowthRate1Year) + + // LP credit should also have grown + let creditAfter1Year = getCreditBalanceForType( + details: getPositionDetails(pid: lpPid, beFailed: false), + vaultType: Type<@FlowToken.Vault>() + ) + Test.assert(creditAfter1Year > FLOWCreditBefore, message: "credit should grow over 1 year") + + // advance 10 more years + Test.moveTime(by: 10.0 * ONE_YEAR) + Test.commitBlock() + + let detailsAfter10Years = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtAfter10Years = getDebitBalanceForType(details: detailsAfter10Years, vaultType: Type<@FlowToken.Vault>()) + let FLOWTotalGrowthRate = (FLOWDebtAfter10Years - FLOWDebtBefore) / FLOWDebtBefore + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // 11-year debt growth = perSecondRate^(31_557_600 * 11) - 1 + // FLOW debit 11-year growth = (1 + 0.017777778 / 31_557_600)^(31_557_600*11) - 1 = 0.21598635 + let expectedFLOWDebt10YearsGrowth = 0.21598635 + Test.assertEqual(expectedFLOWDebt10YearsGrowth, FLOWTotalGrowthRate) +} + +// ============================================================================= +/// Verifies that interest accrues correctly after large time jumps +// ============================================================================= +access(all) +fun test_time_jump_scenarios() { + safeReset() + + // set up LP and borrower + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_FLOW_HOLDER, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 50_000.0, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 50_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + + // borrow 5000 FLOW + borrowFromPosition( + signer: borrower, + positionId: borrowerPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 5000.0, + beFailed: false + ) + + // record state before the 1-day gap + let detailsBefore = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtBefore = getDebitBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) + + // Borrower FLOW rate calculation (KinkInterestCurve) + // utilization = debitBalance / creditBalance + // FLOW: 5000 / 10000 = 0.5 > 0.45 (above kink) + // + // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) + // = (0.5 - 0.45) / (1 - 0.45) = 0.05 / 0.55 = 0.09090909090909... + // + // debitRate = baseRate + slope1 + (slope2 * excessUtilization) + // FLOW: debitRate = 0 + 0.04 + 3.0 * 0.09090909 = 0.3127272727... (31.27% APY) + let expectedFlowDebitRate: UFix128 = 0.31272727 + + // 1-day blockchain halt + Test.moveTime(by: DAY) + Test.commitBlock() + + // first transaction after restart — interest accrual for full gap + let detailsAfter1Day = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtAfter1Day = getDebitBalanceForType(details: detailsAfter1Day, vaultType: Type<@FlowToken.Vault>()) + + Test.assert(FLOWDebtAfter1Day > FLOWDebtBefore, message: "Debt should increase after 1-day gap") + let FLOWDebtDailyGrowth = (FLOWDebtAfter1Day - FLOWDebtBefore) / FLOWDebtBefore + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // dailyGrowth = perSecondRate^86400 - 1 + // FLOW debit daily growth = (1 + 0.31272727 / 31557600)^86400 - 1 = 0.00085660 + let expectedFLOWDebtDailyGrowth = 0.00085660 + Test.assert(equalWithinVariance(expectedFLOWDebtDailyGrowth, FLOWDebtDailyGrowth), + message: "Expected FLOW debt growth rate to be ~\(expectedFLOWDebtDailyGrowth), but got \(FLOWDebtDailyGrowth)") + + // test longer period (7 days) to verify no overflow in calculation + let detailsBefore7Day = getPositionDetails(pid: borrowerPid, beFailed: false) + let FlowDebtBefore7Day = getDebitBalanceForType(details: detailsBefore7Day, vaultType: Type<@FlowToken.Vault>()) + + // 7 days blockchain halt + Test.moveTime(by: 7.0 * DAY) + Test.commitBlock() + + let detailsAfter7Day = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtAfter7Day = getDebitBalanceForType(details: detailsAfter7Day, vaultType: Type<@FlowToken.Vault>()) + Test.assert(FLOWDebtAfter7Day > FlowDebtBefore7Day, message: "FLOW Debt should increase after 7-day gap") + + let FLOWDebtWeeklyGrowth = (FLOWDebtAfter7Day - FlowDebtBefore7Day) / FlowDebtBefore7Day + // weeklyGrowth = perSecondRate^604800 - 1 + // FLOW debit weekly growth = (1 + 0.31272727272 / 31_557_600)^604800 - 1 = 0.00601143 + let expectedFLOWDebtWeeklyGrowth = 0.00601143 + Test.assert(equalWithinVariance(expectedFLOWDebtWeeklyGrowth, FLOWDebtWeeklyGrowth), + message: "Expected FLOW debt growth rate to be ~\(expectedFLOWDebtWeeklyGrowth), but got \(FLOWDebtWeeklyGrowth)") +} \ No newline at end of file diff --git a/cadence/tests/governance_parameters_test.cdc b/cadence/tests/governance_parameters_test.cdc index bdc79d18..53803433 100644 --- a/cadence/tests/governance_parameters_test.cdc +++ b/cadence/tests/governance_parameters_test.cdc @@ -17,7 +17,8 @@ fun test_setGovernanceParams_and_exercise_paths() { // 1) Set insurance swapper let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) diff --git a/cadence/tests/insurance_collection_formula_test.cdc b/cadence/tests/insurance_collection_formula_test.cdc index 27c7cc39..bb0b475c 100644 --- a/cadence/tests/insurance_collection_formula_test.cdc +++ b/cadence/tests/insurance_collection_formula_test.cdc @@ -58,7 +58,12 @@ fun test_collectInsurance_success_fullAmount() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult, Test.beSucceeded()) // set 10% annual debit rate diff --git a/cadence/tests/insurance_collection_test.cdc b/cadence/tests/insurance_collection_test.cdc index 87aa8d96..f64a683f 100644 --- a/cadence/tests/insurance_collection_test.cdc +++ b/cadence/tests/insurance_collection_test.cdc @@ -80,7 +80,12 @@ fun test_collectInsurance_zeroDebitBalance_returnsNil() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult, Test.beSucceeded()) // verify initial insurance fund balance is 0 @@ -129,7 +134,12 @@ fun test_collectInsurance_partialReserves_collectsAvailable() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult, Test.beSucceeded()) // set 90% annual debit rate @@ -182,7 +192,12 @@ fun test_collectInsurance_tinyAmount_roundsToZero_returnsNil() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) // configure insurance swapper with very low rate - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult, Test.beSucceeded()) // set a very low insurance rate @@ -232,7 +247,12 @@ fun test_collectInsurance_success_fullAmount() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult, Test.beSucceeded()) // set 10% annual debit rate @@ -318,10 +338,20 @@ fun test_collectInsurance_multipleTokens() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 20000.0, beFailed: false) // configure insurance swappers for both tokens (both swap to MOET at 1:1) - let moetSwapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let moetSwapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(moetSwapperResult, Test.beSucceeded()) - let flowSwapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, priceRatio: 1.0) + let flowSwapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(flowSwapperResult, Test.beSucceeded()) // set 10% annual debit rates @@ -421,7 +451,12 @@ fun test_collectInsurance_dexOracleSlippageProtection() { // Oracle says FLOW = 1.0 MOET (already set in setup()) // Configure insurance swapper with price ratio = 0.5 (50% deviation from oracle) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, priceRatio: 0.5) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 0.5, + ) Test.expect(swapperResult, Test.beSucceeded()) // set 10% annual debit rate and 10% insurance rate @@ -441,7 +476,12 @@ fun test_collectInsurance_dexOracleSlippageProtection() { Test.assertEqual(0.0, balanceAfterFailure) // Now reconfigure swapper with price ratio = 1.0 (matches oracle, 0% deviation) - let swapperResult2 = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult2 = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult2, Test.beSucceeded()) // collect insurance for FLOW - should SUCCEED now diff --git a/cadence/tests/insurance_rate_test.cdc b/cadence/tests/insurance_rate_test.cdc index 50b52cbb..5830b14e 100644 --- a/cadence/tests/insurance_rate_test.cdc +++ b/cadence/tests/insurance_rate_test.cdc @@ -11,7 +11,6 @@ access(all) fun setup() { deployContracts() createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) - // take snapshot first, then advance time so reset() target is always lower than current height snapshot = getCurrentBlockHeight() // move time by 1 second so Test.reset() works properly before each test @@ -32,7 +31,8 @@ fun test_setInsuranceRate_withoutEGovernanceEntitlement() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -56,7 +56,8 @@ fun test_setInsuranceRate_withEGovernanceEntitlement() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -104,7 +105,8 @@ fun test_setInsuranceRate_rateGreaterThanOne_fails() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -131,7 +133,8 @@ fun test_setInsuranceRate_combinedRateExceedsOne_fails() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -164,7 +167,8 @@ fun test_setStabilityFeeRate_combinedRateExceedsOne_fails() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -197,7 +201,8 @@ fun test_setInsuranceRate_rateLessThanZero_fails() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -223,7 +228,8 @@ fun test_setInsuranceRate_invalidTokenType_fails() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) diff --git a/cadence/tests/insurance_swapper_test.cdc b/cadence/tests/insurance_swapper_test.cdc index 8885b6a6..3a124a6c 100644 --- a/cadence/tests/insurance_swapper_test.cdc +++ b/cadence/tests/insurance_swapper_test.cdc @@ -20,7 +20,8 @@ access(all) fun test_setInsuranceSwapper_success() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -39,7 +40,8 @@ fun test_setInsuranceSwapper_updateExistingSwapper_success() { let initialPriceRatio = 1.0 let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: initialPriceRatio, ) Test.expect(res, Test.beSucceeded()) @@ -48,7 +50,8 @@ fun test_setInsuranceSwapper_updateExistingSwapper_success() { let updatedPriceRatio = 2.0 let updatedRes = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: updatedPriceRatio, ) Test.expect(updatedRes, Test.beSucceeded()) @@ -66,7 +69,8 @@ fun test_removeInsuranceSwapper_success() { // set a swapper let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -94,7 +98,8 @@ fun test_remove_insuranceSwapper_failed() { // set a swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -130,7 +135,8 @@ access(all) fun test_setInsuranceSwapper_withoutEGovernanceEntitlement_fails() { let res = setInsuranceSwapper( signer: alice, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) @@ -148,7 +154,8 @@ fun test_setInsuranceSwapper_invalidTokenTypeIdentifier_fails() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: invalidTokenIdentifier, + swapperInTypeIdentifier: invalidTokenIdentifier, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) @@ -166,7 +173,8 @@ fun test_setInsuranceSwapper_emptyTokenTypeIdentifier_fails() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: emptyTokenIdentifier, + swapperInTypeIdentifier: emptyTokenIdentifier, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) diff --git a/cadence/tests/interest_accrual_integration_test.cdc b/cadence/tests/interest_accrual_integration_test.cdc index eaef69b3..50426b8e 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -43,7 +43,7 @@ access(all) var snapshot: UInt64 = 0 // MOET: FixedCurve (Spread Model) // ----------------------------------------------------------------------------- // In the spread model, the curve defines the DEBIT rate (what borrowers pay). -// The CREDIT rate is derived as: creditRate = debitRate - insuranceRate +// The CREDIT rate is derived as: creditRate = debitRate - protocolRate // This ensures lenders always earn less than borrowers pay, with the // difference going to the insurance pool for protocol solvency. // @@ -172,7 +172,8 @@ fun test_moet_debit_accrues_interest() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -403,7 +404,8 @@ fun test_moet_credit_accrues_interest_with_insurance() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -589,7 +591,8 @@ fun test_flow_debit_accrues_interest() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -777,7 +780,8 @@ fun test_flow_credit_accrues_interest_with_insurance() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -955,7 +959,8 @@ fun test_insurance_deduction_verification() { // Expected Credit Rate: 10% - 1% = 9% let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -1176,7 +1181,8 @@ fun test_combined_all_interest_scenarios() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) diff --git a/cadence/tests/interest_curve_advanced_test.cdc b/cadence/tests/interest_curve_advanced_test.cdc index c1005703..4917f87c 100644 --- a/cadence/tests/interest_curve_advanced_test.cdc +++ b/cadence/tests/interest_curve_advanced_test.cdc @@ -118,8 +118,9 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { // set insurance swapper let res = setInsuranceSwapper( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) diff --git a/cadence/tests/pool_pause_test.cdc b/cadence/tests/pool_pause_test.cdc index 40e0f6a6..042504e0 100644 --- a/cadence/tests/pool_pause_test.cdc +++ b/cadence/tests/pool_pause_test.cdc @@ -70,10 +70,13 @@ fun test_pool_pause_deposit_withdrawal() { Test.expect(depositRes, Test.beFailed()) // Can't withdraw from existing position - let withdrawRes = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [0, FLOW_TOKEN_IDENTIFIER, initialDepositAmount/2.0, false], - user1 + let withdrawRes = withdrawFromPosition( + signer: user1, + positionId: 0, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: initialDepositAmount/2.0, + pullFromTopUpSource: false ) Test.expect(withdrawRes, Test.beFailed()) @@ -102,10 +105,13 @@ fun test_pool_pause_deposit_withdrawal() { Test.expect(depositRes2, Test.beSucceeded()) // Withdrawing from position should still fail during warmup period - let withdrawRes2 = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [0 as UInt64, FLOW_TOKEN_IDENTIFIER, initialDepositAmount/2.0, false], - user1 + let withdrawRes2 = withdrawFromPosition( + signer: user1, + positionId: 0, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: initialDepositAmount/2.0, + pullFromTopUpSource: false ) Test.expect(withdrawRes2, Test.beFailed()) @@ -122,12 +128,13 @@ fun test_pool_pause_deposit_withdrawal() { // --------------------------------------------------------- // Withdrawing from position should now succeed - let withdrawRes3 = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [0 as UInt64, FLOW_TOKEN_IDENTIFIER, initialDepositAmount/2.0, false], - user1 + let withdrawRes3 = withdrawFromPosition( + signer: user1, + positionId: 0, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: initialDepositAmount/2.0, + pullFromTopUpSource: false ) Test.expect(withdrawRes3, Test.beSucceeded()) - - } diff --git a/cadence/tests/position_health_constraints_test.cdc b/cadence/tests/position_health_constraints_test.cdc index 702b0f8e..c80a2bb4 100644 --- a/cadence/tests/position_health_constraints_test.cdc +++ b/cadence/tests/position_health_constraints_test.cdc @@ -190,10 +190,13 @@ fun test_withdraw_fails_when_health_drops_below_one() { // health = 600 / 615.38 ~ 0.975, well below 1.0. // The preflight check enforces that withdrawals cannot reduce health below minHealth, // which prevents health from ever reaching 1.0. - let withdrawRes = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [positionId, FLOW_TOKEN_IDENTIFIER, 250.0, false], - user + let withdrawRes = withdrawFromPosition( + signer: user, + positionId: positionId, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 250.0, + pullFromTopUpSource: false ) Test.expect(withdrawRes, Test.beFailed()) Test.assertError(withdrawRes, errorMessage: "Insufficient funds for withdrawal") diff --git a/cadence/tests/position_lifecycle_unhappy_test.cdc b/cadence/tests/position_lifecycle_unhappy_test.cdc index bc56b949..ad4c79e5 100644 --- a/cadence/tests/position_lifecycle_unhappy_test.cdc +++ b/cadence/tests/position_lifecycle_unhappy_test.cdc @@ -72,20 +72,24 @@ fun testPositionLifecycleBelowMinimumDeposit() { Test.expect(openRes, Test.beSucceeded()) // Attempt to withdraw the exact amount above the minimum - let withdrawResSuccess = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [positionId, FLOW_TOKEN_IDENTIFIER, amountAboveMin, true], - user + let withdrawResSuccess = withdrawFromPosition( + signer: user, + positionId: positionId, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: amountAboveMin, + pullFromTopUpSource: true ) - Test.expect(withdrawResSuccess, Test.beSucceeded()) // Amount should now be exactly the minimum, so withdrawal should fail - let withdrawResFail = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [positionId, FLOW_TOKEN_IDENTIFIER, minimum/2.0, true], - user + let withdrawResFail = withdrawFromPosition( + signer: user, + positionId: positionId, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: minimum/2.0, + pullFromTopUpSource: true ) - Test.expect(withdrawResFail, Test.beFailed()) } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 1a1c2c0f..28eed495 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -54,7 +54,7 @@ access(all) let MAINNET_PROTOCOL_ACCOUNT_ADDRESS: Address = 0x6b00ff876c299c61 access(all) let MAINNET_USDF_HOLDER_ADDRESS: Address = 0xf18b50870aed46ad access(all) let MAINNET_WETH_HOLDER_ADDRESS: Address = 0xf62e3381a164f993 access(all) let MAINNET_WBTC_HOLDER_ADDRESS: Address = 0x47f544294e3b7656 -access(all) let MAINNET_FLOW_HOLDER_ADDRESS: Address = 0xe467b9dd11fa00df +access(all) let MAINNET_FLOW_HOLDER_ADDRESS: Address = 0x92674150c9213fc9 access(all) let MAINNET_USDC_HOLDER_ADDRESS: Address = 0xec6119051f7adc31 /* --- Test execution helpers --- */ @@ -615,13 +615,13 @@ fun borrowFromPosition(signer: Test.TestAccount, positionId: UInt64, tokenTypeId } access(all) -fun withdrawFromPosition(signer: Test.TestAccount, positionId: UInt64, tokenTypeIdentifier: String, amount: UFix64, pullFromTopUpSource: Bool) { +fun withdrawFromPosition(signer: Test.TestAccount, positionId: UInt64, tokenTypeIdentifier: String, receiverVaultStoragePath: StoragePath, amount: UFix64, pullFromTopUpSource: Bool): Test.TransactionResult{ let withdrawRes = _executeTransaction( "./transactions/position-manager/withdraw_from_position.cdc", - [positionId, tokenTypeIdentifier, amount, pullFromTopUpSource], + [positionId, tokenTypeIdentifier, receiverVaultStoragePath, amount, pullFromTopUpSource], signer ) - Test.expect(withdrawRes, Test.beSucceeded()) + return withdrawRes } access(all) @@ -693,12 +693,13 @@ fun setInsuranceRate( access(all) fun setInsuranceSwapper( signer: Test.TestAccount, - tokenTypeIdentifier: String, + swapperInTypeIdentifier: String, + swapperOutTypeIdentifier: String, priceRatio: UFix64, ): Test.TransactionResult { let res = _executeTransaction( "./transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc", - [ tokenTypeIdentifier, priceRatio, tokenTypeIdentifier, MOET_TOKEN_IDENTIFIER], + [ swapperInTypeIdentifier, priceRatio, swapperInTypeIdentifier, swapperOutTypeIdentifier], signer ) return res diff --git a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc index 336df4c5..23f2123e 100644 --- a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc +++ b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc @@ -12,6 +12,7 @@ import "FlowALPModels" transaction( positionId: UInt64, tokenTypeIdentifier: String, + receiverVaultStoragePath: StoragePath, amount: UFix64, pullFromTopUpSource: Bool ) { @@ -19,7 +20,7 @@ transaction( let tokenType: Type let receiverVault: &{FungibleToken.Receiver} - prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + prepare(signer: auth(BorrowValue) &Account) { // Borrow the PositionManager from constant storage path let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath @@ -33,21 +34,8 @@ transaction( self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier: \(tokenTypeIdentifier)") - // Ensure signer has a FlowToken vault to receive withdrawn tokens - if signer.storage.type(at: /storage/flowTokenVault) == nil { - signer.storage.save(<-FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()), to: /storage/flowTokenVault) - } - - // Get receiver for the specific token type - // For FlowToken, use the standard path - if tokenTypeIdentifier == "A.0000000000000003.FlowToken.Vault" { - self.receiverVault = signer.storage.borrow<&{FungibleToken.Receiver}>(from: /storage/flowTokenVault) - ?? panic("Could not borrow FlowToken vault receiver") - } else { - // For other tokens, try to find a matching vault - // This is a simplified approach for testing - panic("Unsupported token type for withdrawal: \(tokenTypeIdentifier)") - } + self.receiverVault = signer.storage.borrow<&{FungibleToken.Receiver}>(from: receiverVaultStoragePath) + ?? panic("Could not borrow receiver vault at \(receiverVaultStoragePath)") } execute { @@ -61,4 +49,4 @@ transaction( // Deposit the withdrawn tokens to the signer's vault self.receiverVault.deposit(from: <-withdrawnVault) } -} +} \ No newline at end of file