diff --git a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc index f3b8c7ed..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/epositionadmin/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 2d793af8..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/epositionadmin/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..c944121e --- /dev/null +++ b/cadence/tests/fork_interest_rate_test.cdc @@ -0,0 +1,621 @@ +#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) + let lpDepositPid = getLastPositionId() + + // 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) + + let borrowerPid = getLastPositionId() + // 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 borrow behavior when a lending pool has no available liquidity. +// ============================================================================= +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 pid = getLastPositionId() + + 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) + var 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) + let lpPid = getLastPositionId() + + // 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) + + let lpDetails = getPositionDetails(pid: lpPid, beFailed: false) + let flowCredit = getCreditBalanceForType(details: lpDetails, vaultType: Type<@FlowToken.Vault>()) + + // 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 = totalDebitBalance * debitRate = 100 * 0.00177777777 = 0.177777777 + // protocolFeeRate = 0.001 + 0.05 = 0.051 + // protocolFeeAmount = 0.177777777 * 0.051 = 0.00906666662 + // creditRate = (0.177777777 - 0.00906666662) / 5000 = 0.00003374222 (0.003374% APY) + + // 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) + + // FLOW LP credit daily growth = (1 + 0.00003374222 / 31_557_600)^86400 - 1 = 0.00000009232 + let expectedFlowCreditDailyGrowth = 0.00000009 + let lpDetailsAfter1Day = getPositionDetails(pid: lpPid, beFailed: false) + let flowCreditAfter1Day = getCreditBalanceForType(details: lpDetailsAfter1Day, vaultType: Type<@FlowToken.Vault>()) + let flowCreditDailyGrowth = (flowCreditAfter1Day - flowCredit) / flowCredit + Test.assertEqual(expectedFlowCreditDailyGrowth, flowCreditDailyGrowth) +} + +// ============================================================================= +/// Verifies protocol behavior when a lending pool has liquidity but no borrowers. +// ============================================================================= +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 lpPid = getLastPositionId() + + // 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= getLastPositionId() + 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 borrowerPid = getLastPositionId() + + // 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) + let lpPid = getLastPositionId() + + // 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) + + let borrowerPid = getLastPositionId() + + // 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 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 borrowerPid = getLastPositionId() + + // 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, DEFAULT_UFIX_VARIANCE), + 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, DEFAULT_UFIX_VARIANCE), + 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 254d4154..7b58952b 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 74571fda..e5144fa2 100644 --- a/cadence/tests/insurance_collection_test.cdc +++ b/cadence/tests/insurance_collection_test.cdc @@ -87,7 +87,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 @@ -112,7 +117,7 @@ fun test_collectInsurance_zeroDebitBalance_returnsNil() { access(all) fun test_collectInsurance_insufficientReserves() { // 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 @@ -192,7 +197,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 @@ -242,7 +252,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 @@ -328,10 +343,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 @@ -431,7 +456,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 @@ -451,7 +481,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 @@ -472,7 +507,7 @@ fun test_collectInsurance_midPeriodRateChange() { // configure the protocol FLOW wallet and the insurance swapper setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, swapperInTypeIdentifier: FLOW_TOKEN_IDENTIFIER, swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) Test.expect(swapperResult, Test.beSucceeded()) // set interest curve 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 fdd70637..77aaa0df 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 263f3241..f3919ceb 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -42,8 +42,8 @@ access(all) var snapshot: UInt64 = 0 // MOET: FixedCurve (Protocol-Fee Spread Model) // ----------------------------------------------------------------------------- -// In the fixed-curve path, the curve defines the DEBIT rate (what borrowers pay). -// The CREDIT rate is derived from the debit rate after protocol fees. +// In the spread model, the curve defines the DEBIT rate (what borrowers pay). +// The CREDIT rate is derived as: creditRate = debitRate - protocolRate // This ensures lenders always earn less than borrowers pay, with the // difference allocated by the configured protocol fee settings. // @@ -173,7 +173,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()) @@ -404,7 +405,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()) @@ -590,7 +592,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()) @@ -778,7 +781,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()) @@ -956,7 +960,8 @@ fun test_insurance_deduction_verification() { // Expected Credit Rate: lower than 10% after protocol fees 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()) @@ -1178,7 +1183,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 c7b467f9..6faa2182 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 d1989bda..115a512e 100644 --- a/cadence/tests/pool_pause_test.cdc +++ b/cadence/tests/pool_pause_test.cdc @@ -78,10 +78,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()) @@ -110,10 +113,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()) @@ -130,12 +136,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 8b8d84ed..9ad2933b 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 1d422c6c..e626b4a4 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 --- */ @@ -701,13 +701,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], + "./transactions/flow-alp/epositionadmin/withdraw_from_position.cdc", + [positionId, tokenTypeIdentifier, receiverVaultStoragePath, amount, pullFromTopUpSource], signer ) - Test.expect(withdrawRes, Test.beSucceeded()) + return withdrawRes } access(all) @@ -779,12 +779,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/egovernance/set_insurance_swapper_mock.cdc", - [ tokenTypeIdentifier, priceRatio, tokenTypeIdentifier, MOET_TOKEN_IDENTIFIER], + [ swapperInTypeIdentifier, priceRatio, swapperInTypeIdentifier, swapperOutTypeIdentifier], signer ) return res diff --git a/cadence/tests/transactions/flow-alp/epositionadmin/withdraw_from_position.cdc b/cadence/tests/transactions/flow-alp/epositionadmin/withdraw_from_position.cdc index 7ad60eb7..3ae08679 100644 --- a/cadence/tests/transactions/flow-alp/epositionadmin/withdraw_from_position.cdc +++ b/cadence/tests/transactions/flow-alp/epositionadmin/withdraw_from_position.cdc @@ -7,6 +7,7 @@ import "FungibleToken" transaction( positionID: UInt64, tokenTypeIdentifier: String, + receiverVaultStoragePath: StoragePath, amount: UFix64, pullFromTopUpSource: Bool ) { @@ -21,9 +22,8 @@ transaction( self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") - let cap = signer.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) - self.receiverRef = cap.borrow() - ?? panic("Could not borrow receiver ref from /public/flowTokenReceiver") + self.receiverRef = signer.storage.borrow<&{FungibleToken.Receiver}>(from: receiverVaultStoragePath) + ?? panic("Could not borrow receiver vault at \(receiverVaultStoragePath)") } execute { diff --git a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc deleted file mode 100644 index 013a63aa..00000000 --- a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc +++ /dev/null @@ -1,65 +0,0 @@ -import "FungibleToken" -import "FlowToken" -import "FlowALPv0" -import "FlowALPPositionResources" -import "FlowALPModels" - -/// TEST TRANSACTION - DO NOT USE IN PRODUCTION -/// -/// Withdraws the specified amount and token type from the position. -/// This will fail if the withdrawal would leave the position below the minimum balance requirement -/// (unless withdrawing all funds to close the position). -/// -transaction( - positionId: UInt64, - tokenTypeIdentifier: String, - amount: UFix64, - pullFromTopUpSource: Bool -) { - let position: auth(FungibleToken.Withdraw) &FlowALPPositionResources.Position - let tokenType: Type - let receiverVault: &{FungibleToken.Receiver} - - prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { - // Borrow the PositionManager from constant storage path - let manager = signer.storage.borrow( - from: FlowALPv0.PositionStoragePath - ) - ?? panic("Could not find PositionManager in signer's storage") - - // Borrow the position with withdraw entitlement - self.position = manager.borrowAuthorizedPosition(pid: positionId) - - // Parse the token type - 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)") - } - } - - execute { - // Withdraw from the position with optional top-up pulling - let withdrawnVault <- self.position.withdrawAndPull( - type: self.tokenType, - amount: amount, - pullFromTopUpSource: pullFromTopUpSource - ) - - // Deposit the withdrawn tokens to the signer's vault - self.receiverVault.deposit(from: <-withdrawnVault) - } -}